2026-10-20 15:48:38 +08:00
import assert from 'node:assert/strict'
import { verifyFleetCrossCustomer } from './fleet-cross-customer-acceptance.mjs'
2026-10-20 16:45:04 +08:00
import { run as createQualifiedMerchant } from './merchant-qualification-fixture.mjs'
import { nativeRelabelCreateBody } from '../overseas-relabel-real-acceptance.mjs'
import { finishFleetTrip } from './fleet-trip-fulfillment.mjs'
2026-10-20 15:48:38 +08:00
const ROOT = '/api/platform/v1/last-leg' , MODULE = 'dispatch-workflow-extensions'
const OPTIONS = { scenario : MODULE , deploymentBlocker : true }
const clone = value => structuredClone ( value )
const id = value => String ( value ?? '' )
export const extensionMatrix = Object . freeze ([
{ scenario : 'dual-destination' , scope : 'Two fresh customers, two distinct real FBA destinations, one main/trip; distinct fee inputs and isolated execution documents' , readiness : 'READY' },
{ scenario : 'reviews' , scope : 'Both rejection/resubmission rounds, zero-pick return and cargo/resource changes, stale/version/idempotency conflicts' , readiness : 'READY' },
2026-10-20 16:45:04 +08:00
{ scenario : 'relabel' , scope : 'Fresh three-box customer receipt, confirmed zero-scan cancellation, two applications sharing one pallet and native rearrangement' , readiness : 'READY_FORMAL_THREE_BOX_RECEIPT' },
2026-10-20 15:48:38 +08:00
{ scenario : 'competition' , scope : 'Two-request Admin/PDA replay race, occupied resources and unarranged departure gates; first-execution race remains untested' , readiness : 'READY_BOUNDED_REPLAY' },
{ scenario : 'bol' , scope : 'Fresh same-destination cargo, empty/same/different formal appointment allocations and actual PDF conservation' , readiness : 'READY_FORMAL_APPOINTMENTS' },
])
export function assertDualDestinationFixture ( fixture ) {
assert . equal ( fixture . members ? . length , 2 )
assert . equal ( new Set ( fixture . members . map ( row => id ( row . customerId ))). size , 2 )
assert . equal ( new Set ( fixture . members . map ( row => row . destination ? . key )). size , 2 )
assert ( fixture . members . every ( row => row . destination ? . key && row . receiptVerified === true ))
assert ( fixture . tripProof ? . stops ? . length >= 3 )
for ( const member of fixture . members )
assert ( fixture . tripProof . stops . some ( stop => stop . destinationKey === member . destination . key ))
}
export function feeCents ( value ) {
assert ([ 'string' , 'number' ]. includes ( typeof value ) && /^(0|[1-9]\d*)(\.\d{1,2})?$/ . test ( String ( value )), 'Non-canonical fee amount' )
const [ whole , fraction = '' ] = String ( value ). split ( '.' )
const cents = Number ( whole ) * 100 + Number ( fraction . padEnd ( 2 , '0' ))
assert ( Number . isSafeInteger ( cents )); return cents
}
export function applyIndependentFees ( body , groups , members , marker ) {
assert . equal ( groups ? . length , 2 ); assert . equal ( body . groups ? . length , 2 )
assert . equal ( new Set ( groups . map ( row => row . groupKey )). size , 2 )
assert . equal ( new Set ( groups . map ( row => id ( row . customerId ))). size , 2 )
const centsByCustomer = new Map ( members . map (( member , index ) => [ member . customerId , index === 0 ? 3125 : 4775 ]))
const expected = {}
for ( const group of body . groups ) {
const matches = groups . filter ( row => row . groupKey === group . groupKey )
assert . equal ( matches . length , 1 )
const customerId = id ( matches [ 0 ]. customerId ), cents = centsByCustomer . get ( customerId )
assert ( cents && group . fees ? . length > 0 , 'Explicit current-customer fee services required' )
assert ( ! expected [ customerId ], 'Duplicated customer fee group' )
for ( const fee of group . fees ) { fee . amount = cents / 100 ; fee . feeName = ` ${ marker } TEST合成客户独立费用` }
expected [ customerId ] = group . fees . map ( fee => ({ feeType : fee . feeType , currency : fee . currency , cents : feeCents ( fee . amount ) }))
}
return expected
}
/** Public master-data reads only; no fixture writes until both independent US FBA masters are verified. */
export async function discoverSecondDestination ( context , primaryId ) {
const get = async path => {
const response = await context . request ( 'GET' , path )
assert . equal ( response . http , 200 ); assert . equal ( String ( response . body ? . code ), '200' ); return response . body . data
}
const primary = await get ( `/api/platform/v1/warehouse-admin/ ${ encodeURIComponent ( primaryId ) } ?tenantId=0` )
const country = primary . addresses ? . find ( row => row . addressLine1 ) ? . countryCode
assert ( country && primary . warehouse ? . warehouseType === 'FBA' && primary . warehouse . status === 'ACTIVE' )
let total , scanned = 0
const seen = new Set ()
for ( let page = 1 ; page <= 100 ; page ++ ) {
const data = await get ( `/api/platform/v1/warehouse-admin? ${ new URLSearchParams ({ tenantId : '0' , page : String ( page ), size : '200' , warehouseType : 'FBA' , status : 'ACTIVE' , countryCode : country } )}` )
assert ( Array . isArray ( data . records )); assert . equal ( Number ( data . page ), page ); assert . equal ( Number ( data . size ), 200 )
assert ( Number . isSafeInteger ( Number ( data . total )) && Number ( data . total ) >= 0 )
total ??= Number ( data . total ); assert . equal ( Number ( data . total ), total )
assert ( data . records . length > 0 || scanned === total )
for ( const row of data . records ) {
assert ( row . id && ! seen . has ( id ( row . id ))); seen . add ( id ( row . id )); scanned ++
if ( id ( row . id ) === id ( primaryId )) continue
const candidate = await get ( `/api/platform/v1/warehouse-admin/ ${ encodeURIComponent ( row . id ) } ?tenantId=0` )
if ( candidate . warehouse ? . warehouseType !== 'FBA' || candidate . warehouse . status !== 'ACTIVE'
|| id ( candidate . warehouse . tenantId ) !== '0' || candidate . warehouse . warehouseCode === primary . warehouse . warehouseCode ) continue
if ( candidate . addresses ? . some ( address => address . addressLine1 && address . countryCode === country )) return id ( row . id )
}
if ( scanned === total ) break
}
throw Error ( 'No second distinct public active FBA destination with a real same-country address; no fixture created' )
}
/** Reuse the formal full-chain verifier; only explicitly supplied synthetic fee amounts differ by customer. */
export async function verifyDualDestination ( context , fixture , options ) {
if ( ! options . bol ) assertDualDestinationFixture ( fixture )
else {
assert . equal ( fixture . members . length , 2 ); assert . equal ( new Set ( fixture . members . map ( row => row . customerId )). size , 2 )
assert . equal ( new Set ( fixture . members . map ( row => row . destination . key )). size , 1 )
assert ( fixture . members . every ( row => row . receiptVerified === true ))
}
const proof = context . createdResources . dispatchWorkflowExtensions = { scenario : options . bol || 'dual-destination' , marker : context . marker ,
dispatchNo : fixture . dispatchNo , status : 'CHECKING' , syntheticTestMaterials : true , physicalMeasurementVerified : false ,
hardwareVerified : false , midTransactionRollback : 'NOT_TESTED_NO_CONTROLLED_INJECTION' , expectedFees : {}, isolation : [] }
const original = context . request
2026-10-20 16:45:04 +08:00
let arrangement , deferredFailure , raceDone = false , firstRaceDone = false , unarrangedDone = false
2026-10-20 15:48:38 +08:00
const mainPath = ` ${ ROOT } /dispatches/ ${ encodeURIComponent ( fixture . dispatchNo ) } `
const pdaPath = `/api/pda/v1/last-leg/dispatches/ ${ encodeURIComponent ( fixture . dispatchNo ) } `
context . request = async function ( method , path , body , ... rest ) {
if ( deferredFailure ) throw deferredFailure
if ( method === 'POST' && path === ` ${ ROOT } /dispatches/ ${ encodeURIComponent ( fixture . dispatchNo ) } /transfer-delivery` ) {
proof . expectedFees = applyIndependentFees ( body , arrangement ? . groups , fixture . members , context . marker )
proof . arrangeRequest = clone ( body ); await context . checkpoint ()
}
2026-10-20 16:45:04 +08:00
let response
if ( options . firstCompetition && ! firstRaceDone && method === 'POST' && path === ` ${ pdaPath } /loading-complete`
&& fixture . tripProof . loadingCommand ? . body ? . idempotencyKey === body ? . idempotencyKey ) {
firstRaceDone = true
const outcome = await executeFirstLoadingRace ( context , fixture , options , original . bind ( this ), body )
response = outcome . pdaResponse
if ( outcome . error ) deferredFailure = outcome . error
} else response = await original . call ( this , method , path , body , ... rest )
2026-10-20 15:48:38 +08:00
if ( method === 'GET' && path === ` ${ ROOT } /dispatches/ ${ encodeURIComponent ( fixture . dispatchNo ) } /delivery-arrangement`
&& response . http === 200 && String ( response . body ? . code ) === '200' ) arrangement = clone ( response . body . data )
// 写请求已收到确定回执后,只做独立扩展;后续核验失败延至下一读请求抛出,不能把已知成功伪报成未知写。
2026-10-20 16:45:04 +08:00
if ( ! deferredFailure && response . http === 200 && String ( response . body ? . code ) === '200' ) {
2026-10-20 15:48:38 +08:00
try {
if ( options . competition && method === 'POST' && path === ` ${ pdaPath } /loading-complete` && ! raceDone
&& response . body . data ? . replayed !== true && response . body . data ? . stage === 'AFTER_LOADING' ) {
raceDone = true
await verifyLoadingReplayRace ( context , fixture , options , original . bind ( this ), body , response . body . data )
}
if ( options . competition && method === 'GET' && path === ` ${ mainPath } /delivery-arrangement` && ! unarrangedDone
&& response . body . data ? . stage === 'AFTER_LOADING' && response . body . data ? . arrangement == null ) {
unarrangedDone = true
await verifyUnarrangedDeparture ( context , fixture , options , original . bind ( this ), response . body . data )
}
if ( method === 'PUT' && body ? . idempotencyKey === ` ${ context . marker } -CROSS-SIGN-1` ) {
const fence = proof . completionReadFence = { status : 'READING' , attempts : [], writeReceiptPreserved : true }
for ( let attempt = 1 ; attempt <= 60 ; attempt ++ ) {
const current = await original . call ( this , 'GET' , mainPath )
assert . equal ( current . http , 200 ); assert . equal ( String ( current . body ? . code ), '200' )
assert . equal ( current . body . data ? . dispatchNo , fixture . dispatchNo )
fence . attempts . push ({ attempt , stage : current . body . data . stage , recordVersion : current . body . data . recordVersion ,
requestId : current . requestId , traceId : current . traceId })
if ( current . body . data . stage === 'DELIVERED' ) { fence . status = 'DELIVERED' ; break }
assert . equal ( current . body . data . stage , 'DELIVERING' )
await context . checkpoint (); await new Promise ( resolve => setTimeout ( resolve , 500 ))
}
assert . equal ( fence . status , 'DELIVERED' , 'Automatic completion did not settle within 30 seconds' )
await context . checkpoint ()
}
} catch ( error ) { deferredFailure = error }
}
return response
}
let verified
try { verified = await verifyFleetCrossCustomer ( context , fixture , options ) }
finally { context . request = original }
assert . equal ( verified . status , 'VERIFIED' )
const final = verified . snapshots . FINAL
assert . equal ( new Set ( final . dispatch . documents . deliveries . map ( row => row . deliveryNo )). size , 2 )
assert . equal ( new Set ( final . dispatch . documents . outbounds . map ( row => row . outboundNo )). size , 2 )
const feeIds = new Set ()
for ( const [ index , member ] of fixture . members . entries ()) {
const delivery = final . members [ index ]. delivery
assert . equal ( id ( delivery . order . customerId ), member . customerId )
assert . equal ( delivery . order . destinationWarehouseCode , member . destination . warehouseCode )
assert . equal ( Number ( delivery . order . packageCount ), 1 ); assert . equal ( Number ( delivery . order . palletCount ), 1 )
const expected = proof . expectedFees [ member . customerId ]
assert ( expected ? . length > 0 ); assert . equal ( delivery . fees ? . length , expected . length )
for ( const fee of delivery . fees ) {
assert ( fee . id && ! feeIds . has ( id ( fee . id ))); feeIds . add ( id ( fee . id ))
assert ( expected . some ( row => row . feeType === fee . feeType && row . currency === fee . currency && row . cents === feeCents ( fee . amount )))
assert . equal ( fee . feeDirection , 'RECEIVABLE' ); assert . equal ( fee . sourceStatus , 'ACTIVE' )
}
const other = fixture . members [ 1 - index ]
const path = ` ${ ROOT } /delivery-orders/ ${ encodeURIComponent ( delivery . order . deliveryNo ) } ?customerId= ${ encodeURIComponent ( other . customerId ) } `
const response = await context . request ( 'GET' , path )
const denied = [ 200 , 403 , 404 ]. includes ( response . http ) && [ 'A0403' , 'A0404' ]. includes ( String ( response . body ? . code ))
context . assert ( MODULE , 'Foreign customer cannot read this delivery or its independent fees' , response , denied , '' , OPTIONS )
assert ( denied )
proof . isolation . push ({ customerId : member . customerId , wrongCustomerId : other . customerId , deliveryNo : delivery . order . deliveryNo ,
http : response . http , code : response . body . code , requestId : response . requestId , traceId : response . traceId })
}
assert . notDeepEqual ( proof . expectedFees [ fixture . members [ 0 ]. customerId ], proof . expectedFees [ fixture . members [ 1 ]. customerId ])
proof . status = 'VERIFIED' ; await context . checkpoint (); return verified
}
const receipt = response => ({ http : response . http , code : response . body ? . code , message : response . body ? . message ,
requestId : response . requestId , traceId : response . traceId })
async function competitionSnapshot ( context , fixture , { readInventory }, send ) {
const path = ` ${ ROOT } /dispatches/ ${ encodeURIComponent ( fixture . dispatchNo ) } `
const state = {}
for ( const [ name , target ] of [[ 'main' , path ], [ 'items' , ` ${ path } /items` ], [ 'loading' , ` ${ path } /loading` ],
[ 'trip' , ` ${ ROOT } /fleet/trips/ ${ encodeURIComponent ( fixture . tripNo ) } ` ]]) {
const response = await send ( 'GET' , target )
assert . equal ( response . http , 200 ); assert . equal ( String ( response . body ? . code ), '200' ); state [ name ] = response . body . data
}
state . stock = []
for ( const member of fixture . members ) state . stock . push ( await readInventory ({ marker : context . marker ,
customerId : member . customerId , warehouseId : fixture . warehouseId }))
return state
}
/** 仅两个请求同时发送;使用已成功装车的原键,明确区别首次执行竞争。 */
export async function verifyLoadingReplayRace ( context , fixture , options , send , body , acknowledged ) {
const proof = context . createdResources . dispatchWorkflowCompetition
assert ( proof && proof . status === 'CHECKING' )
const path = ` ${ ROOT } /dispatches/ ${ encodeURIComponent ( fixture . dispatchNo ) } /loading-complete`
const pda = `/api/pda/v1/last-leg/dispatches/ ${ encodeURIComponent ( fixture . dispatchNo ) } /loading-complete`
const race = proof . loadingReplayRace = { status : 'INTENT' , scope : 'ACKNOWLEDGED_COMMAND_TWO_ACTOR_REPLAY' ,
firstExecutionRace : 'NOT_TESTED' , body : clone ( body ), before : await competitionSnapshot ( context , fixture , options , send ) }
await context . checkpoint ()
const results = await Promise . allSettled ([ send ( 'POST' , pda , clone ( body ), fixture . pdaToken ), send ( 'POST' , path , clone ( body ))])
race . responses = results . map ( result => result . status === 'fulfilled' ? receipt ( result . value ) : { outcome : 'UNKNOWN_RESULT' })
race . status = results . every ( result => result . status === 'fulfilled' ) ? 'RESPONDED' : 'UNKNOWN_RESULT'
await context . checkpoint ()
for ( const result of results ) if ( result . status === 'rejected' ) throw result . reason
const [ pdaResult , adminResult ] = results . map ( result => result . value )
assert . equal ( String ( pdaResult . body ? . code ), '200' ); assert . equal ( pdaResult . body ? . data ? . replayed , true )
assert . deepEqual ( pdaResult . body . data . result , acknowledged . result )
assert . equal ( id ( pdaResult . body . data . recordVersion ), id ( acknowledged . recordVersion ))
assert . equal ( String ( adminResult . body ? . code ), 'A0403' ); assert . match ( adminResult . body ? . message || '' , /操作员/ )
race . after = await competitionSnapshot ( context , fixture , options , send )
assert . deepEqual ( race . after , race . before )
context . assert ( MODULE , 'Concurrent Admin/PDA original-key replays preserve the one loading result and complete inventories' ,
pdaResult , true , '' , OPTIONS )
race . status = 'VERIFIED' ; await context . checkpoint ()
}
2026-10-20 16:45:04 +08:00
/** 首次命令使用两个不同键和同一真实版本;胜者必须唯一,真实PDA回执原样交还调用者。 */
export async function executeFirstLoadingRace ( context , fixture , options , send , body ) {
const proof = context . createdResources . dispatchWorkflowCompetition
assert ( proof && proof . status === 'CHECKING' )
const adminBody = { ... clone ( body ), idempotencyKey : ` ${ context . marker } -EXT-FIRST-ADMIN-LOAD` }
const race = proof . firstLoadingRace = { status : 'INTENT' , pdaBody : clone ( body ), adminBody ,
before : await competitionSnapshot ( context , fixture , options , send ) }
assert . equal ( race . before . main . stage , 'LOADING' ); assert . equal ( Number ( race . before . main . counts . pickedPackageCount ), 2 )
assert . equal ( id ( race . before . main . recordVersion ), id ( body . recordVersion )); await context . checkpoint ()
const results = await Promise . allSettled ([
send ( 'POST' , `/api/pda/v1/last-leg/dispatches/ ${ encodeURIComponent ( fixture . dispatchNo ) } /loading-complete` , clone ( body ), fixture . pdaToken ),
send ( 'POST' , ` ${ ROOT } /dispatches/ ${ encodeURIComponent ( fixture . dispatchNo ) } /loading-complete` , adminBody ),
])
race . responses = results . map ( result => result . status === 'fulfilled' ? receipt ( result . value ) : { outcome : 'UNKNOWN_RESULT' })
race . status = results . every ( result => result . status === 'fulfilled' ) ? 'RESPONDED' : 'UNKNOWN_RESULT' ; await context . checkpoint ()
for ( const result of results ) if ( result . status === 'rejected' ) throw result . reason
const responses = results . map ( result => result . value )
try {
const winnerIndexes = responses . map (( response , index ) => String ( response . body ? . code ) === '200' ? index : - 1 ). filter ( index => index >= 0 )
assert . equal ( winnerIndexes . length , 1 )
const winnerIndex = winnerIndexes [ 0 ], winner = responses [ winnerIndex ], loser = responses [ 1 - winnerIndex ]
assert . equal ( String ( loser . body ? . code ), 'A0409' )
assert . equal ( winner . body . data . stage , 'AFTER_LOADING' )
assert . equal ( winner . body . data . result ? . confirmedLoad ? . manifestHash , body . manifestHash )
assert . equal ( BigInt ( winner . body . data . recordVersion ), BigInt ( body . recordVersion ) + 1 n )
race . after = await competitionSnapshot ( context , fixture , options , send )
assert . equal ( race . after . main . stage , 'AFTER_LOADING' )
assert . equal ( id ( race . after . main . recordVersion ), id ( winner . body . data . recordVersion ))
assert . deepEqual ( race . after . stock , race . before . stock )
assert . equal ( Number ( race . after . main . counts . packageCount ), 2 )
assert . equal ( Number ( race . after . main . counts . pickedPackageCount ), 2 )
race . winner = winnerIndex === 0 ? 'PDA' : 'ADMIN' ; race . winnerResult = clone ( winner . body . data )
race . status = 'VERIFIED' ; proof . firstExecutionRace = 'VERIFIED'
context . assert ( MODULE , 'First valid Admin/PDA loading race commits exactly one version and preserves complete inventories' , winner , true , '' , OPTIONS )
await context . checkpoint ()
if ( winnerIndex !== 0 ) return { pdaResponse : responses [ 0 ], error : Error ( 'Admin won the first loading race; original PDA rejection retained, no fulfillment replay' ) }
return { pdaResponse : responses [ 0 ] }
} catch ( error ) {
race . verificationError = error . message ; await context . checkpoint ()
return { pdaResponse : responses [ 0 ], error }
}
}
2026-10-20 15:48:38 +08:00
async function verifyUnarrangedDeparture ( context , fixture , options , send , arrangement ) {
const proof = context . createdResources . dispatchWorkflowCompetition
const state = proof . unarrangedDeparture = { status : 'INTENT' , before : await competitionSnapshot ( context , fixture , options , send ),
body : { recordVersion : arrangement . recordVersion , idempotencyKey : ` ${ context . marker } -EXT-NO-ARRANGEMENT-DEPART` , remark : 'TEST装车后未安排派送必须拒绝' } }
assert . equal ( state . before . main . stage , 'AFTER_LOADING' ); assert . equal ( state . before . main . documents ? . deliveries ? . length , 0 )
await context . checkpoint ()
const response = await send ( 'POST' , `/api/pda/v1/last-leg/dispatches/ ${ encodeURIComponent ( fixture . dispatchNo ) } /depart` , state . body , fixture . pdaToken )
state . response = receipt ( response ); await context . checkpoint ()
assert . equal ( String ( response . body ? . code ), 'A0409' ); assert . match ( response . body ? . message || '' , /派送|安排/ )
state . after = await competitionSnapshot ( context , fixture , options , send ); assert . deepEqual ( state . after , state . before )
context . assert ( MODULE , 'Loaded but unarranged departure is rejected without inventory or execution mutation' , response , true , '' , OPTIONS )
state . status = 'VERIFIED' ; await context . checkpoint ()
}
export async function verifyCompetitionExtension ( context , fixture , options ) {
const proof = context . createdResources . dispatchWorkflowCompetition = { marker : context . marker , dispatchNo : fixture . dispatchNo ,
status : 'CHECKING' , firstExecutionRace : 'NOT_TESTED' , arbitraryPrivilegeMatrix : 'NOT_TESTED_NO_PERMISSION_MUTATION' }
const before = await competitionSnapshot ( context , fixture , options , context . request . bind ( context ))
const body = { idempotencyKey : ` ${ context . marker } -EXT-OCCUPIED-RESOURCE` , warehouseId : fixture . warehouseId ,
vehicle : { mode : 'MASTER' , vehicleId : fixture . tripProof . vehicleId },
driver : { mode : 'MASTER' , driverId : fixture . tripProof . driverId , evidence : { expectedTaskEndAt : fixture . tripProof . expectedDriverEndAt } },
plannedDepartureAt : new Date ( Date . now () + 3600000 ). toISOString (), estimatedArrivalAt : null ,
remark : ` ${ context . marker } TEST已占用资源不能分配另一空车` , cargo : [], batchAllocations : [] }
proof . resourceConflict = { status : 'INTENT' , body : clone ( body ), before }; await context . checkpoint ()
const response = await context . request ( 'POST' , ` ${ ROOT } /dispatches` , body )
proof . resourceConflict . response = receipt ( response ); await context . checkpoint ()
assert . equal ( String ( response . body ? . code ), 'A0409' ); assert . match ( response . body ? . message || '' , /车辆.*占用|司机.*占用/ )
proof . resourceConflict . after = await competitionSnapshot ( context , fixture , options , context . request . bind ( context ))
assert . deepEqual ( proof . resourceConflict . after , before )
context . assert ( MODULE , 'Occupied vehicle/driver reject a second native main without changing the existing trip or inventory' , response , true , '' , OPTIONS )
proof . resourceConflict . status = 'VERIFIED'
const verified = await verifyDualDestination ( context , fixture , { ... options , competition : true })
assert . equal ( proof . loadingReplayRace ? . status , 'VERIFIED' ); assert . equal ( proof . unarrangedDeparture ? . status , 'VERIFIED' )
2026-10-20 16:45:04 +08:00
if ( options . firstCompetition ) assert . equal ( proof . firstLoadingRace ? . status , 'VERIFIED' )
proof . status = options . firstCompetition ? 'VERIFIED_FIRST_EXECUTION_AND_REPLAY' : 'VERIFIED_BOUNDED_SCOPE'
await context . checkpoint (); return verified
2026-10-20 15:48:38 +08:00
}
2026-10-20 16:45:04 +08:00
export const verifyFirstCompetitionExtension = ( context , fixture , options ) => verifyCompetitionExtension ( context , fixture , { ... options , firstCompetition : true })
2026-10-20 15:48:38 +08:00
/** 仅使用本轮PDA实收的一箱来源,正式创建预约并向原生建单返回实际份额。 */
export async function prepareBolAppointments ( context , members , scenario , warehouseId ) {
assert ([ 'bol-empty' , 'bol-same' , 'bol-different' ]. includes ( scenario ))
const proof = context . createdResources . dispatchWorkflowBolAppointments = { scenario , status : 'PREPARING' , appointments : [], allocations : [] }
if ( scenario === 'bol-empty' ) { proof . status = 'NO_APPOINTMENT' ; return [] }
const groups = scenario === 'bol-same' ? [ members ] : members . map ( member => [ member ])
for ( const [ index , group ] of groups . entries ()) {
for ( const member of group ) {
const response = await context . request ( 'GET' , ` ${ ROOT } /warehouse-transfers/ ${ encodeURIComponent ( member . incomingNo ) } ?customerId= ${ encodeURIComponent ( member . customerId ) } ` )
assert . equal ( response . http , 200 ); assert . equal ( String ( response . body ? . code ), '200' )
const transfer = response . body . data
assert . equal ( transfer . transferNo , member . incomingNo ); assert . equal ( transfer . inboundCode , member . incomingNo )
assert . equal ( id ( transfer . inboundOrderId ), id ( member . receipt . balance . inboundOrderId ))
assert . equal ( id ( transfer . operatingWarehouseId ), warehouseId )
assert ( transfer . lastlegOrderId && member . selection . planPalletId && member . receiptVerified )
member . appointmentSourceKey = `ORDER: ${ transfer . lastlegOrderId } `
}
const refs = group . map ( member => {
return { customerId : member . customerId , sourceKey : member . appointmentSourceKey , destinationKey : member . destination . key }
})
const confirmationNo = `TEST-ISA- ${ context . marker . slice ( - 12 ) } - ${ index + 1 } `
const body = { sources : refs , originWarehouseId : warehouseId , fbaWarehouseCode : group [ 0 ]. destination . warehouseCode ,
details : { appointmentStatus : 'SUCCESS' , confirmationNo , appointmentAt : new Date ( Date . now () + 48 * 3600000 ). toISOString (). slice ( 0 , 19 ),
appointmentEndAt : new Date ( Date . now () + 50 * 3600000 ). toISOString (). slice ( 0 , 19 ), appointmentRemark : ` ${ context . marker } TEST合成预约确认资料` },
idempotencyKey : ` ${ context . marker } -BOL-AP- ${ index + 1 } ` }
const record = { status : 'INTENT' , body : clone ( body ) }; proof . appointments . push ( record ); await context . checkpoint ()
const response = await context . request ( 'POST' , ` ${ ROOT } /appointment-planning/appointments` , body )
record . response = receipt ( response ); record . status = 'RESPONDED' ; await context . checkpoint ()
const data = context . apiSuccess ( MODULE , 'Create own formal FBA appointment for exact received quantities' , response , '' , OPTIONS )
assert . equal ( String ( response . body ? . code ), '200' ); assert ( data ? . batchKey && data . appointmentNo )
assert . equal ( data . confirmationNo , confirmationNo ); assert . equal ( data . appointmentStatus , 'SUCCESS' )
assert . equal ( data . items . length , group . length ); assert ( Number . isSafeInteger ( data . version ) && data . version >= 0 )
record . data = clone ( data )
for ( const member of group ) {
const matches = data . items . filter ( item => id ( item . customerId ) === member . customerId && item . sourceKey === member . appointmentSourceKey )
assert . equal ( matches . length , 1 ); assert . equal ( Number ( matches [ 0 ]. packageCount ), 1 )
assert . equal ( matches [ 0 ]. destinationKey , member . destination . key )
proof . allocations . push ({ planPalletId : member . selection . planPalletId , batchKey : data . batchKey , customerId : member . customerId ,
sourceKey : matches [ 0 ]. sourceKey , destinationKey : matches [ 0 ]. destinationKey , packageCount : 1 , recordVersion : data . version })
}
record . status = 'VERIFIED' ; await context . checkpoint ()
}
proof . status = 'VERIFIED' ; return clone ( proof . allocations )
}
export function assertBolPages ( pages , fixture , appointments , scenario ) {
const expected = scenario === 'bol-different' ? 2 : 1
assert . equal ( pages . length , expected )
const counts = [], isas = [], flattened = pages . map ( page => page . replace ( /\s/g , '' ))
for ( const page of pages ) {
assert . match ( page , /BILL OF LADING/ )
const total = page . match ( /Total\s+CTNS:\s*(\d+)/ )
assert ( total , 'Printed total box count missing' ); counts . push ( Number ( total [ 1 ]))
const isaLine = page . split ( /\r?\n/ ). find ( line => line . includes ( 'ISA #:' ))
assert ( isaLine !== undefined ); isas . push ( isaLine . split ( 'ISA #:' )[ 1 ]. trim ())
}
assert . equal ( counts . reduce (( sum , count ) => sum + count , 0 ), 2 )
// 现行窄托盘列会省略长托盘码;用完整入仓码核对原实收来源,每个来源在本夹具中严格对应一箱一实托。
for ( const member of fixture . members ) {
assert ( member . incomingNo && member . palletNo )
assert . equal ( flattened . reduce (( sum , page ) => sum + page . split ( member . incomingNo ). length - 1 , 0 ), 1 )
}
const expectedIsas = appointments . appointments . map ( row => row . data . confirmationNo )
assert . deepEqual ([... isas ]. sort (), scenario === 'bol-empty' ? [ '' ] : [... expectedIsas ]. sort ())
if ( scenario !== 'bol-different' ) assert . deepEqual ( counts , [ 2 ])
else assert . deepEqual ( counts , [ 1 , 1 ])
return { pageCount : pages . length , totalPackages : 2 , packagesPerBol : counts , confirmationNos : isas ,
eachSourcePrintedOnce : true , physicalPalletMapping : 'ONE_BOX_ONE_PALLET_PER_SOURCE' , palletColumn : 'LONG_CODES_TRUNCATED_BY_EXISTING_TEMPLATE' }
}
export async function verifyBolExtension ( context , fixture , options ) {
assert ( typeof options . inspectBol === 'function' )
const proof = context . createdResources . dispatchWorkflowBol = { scenario : options . bol , status : 'CHECKING' ,
physicalMeasurementVerified : false , hardwareVerified : false }
proof . before = await competitionSnapshot ( context , fixture , options , context . request . bind ( context ))
const pages = await options . inspectBol ( context , fixture )
proof . printed = assertBolPages ( pages , fixture , context . createdResources . dispatchWorkflowBolAppointments , options . bol )
proof . after = await competitionSnapshot ( context , fixture , options , context . request . bind ( context ))
assert . deepEqual ( proof . after , proof . before )
context . assert ( MODULE , 'Actual BOL PDF preserves appointment shares, cross-customer grouping and complete inventory facts' ,
{ http : 200 }, true , '' , OPTIONS )
proof . status = 'VERIFIED' ; await context . checkpoint ()
return verifyDualDestination ( context , fixture , options )
}
/** Exercise edits only on this fresh zero-picked main, then leave a new fully approved round for the formal chain. */
export async function exerciseReviewLifecycle ( context , fixture , { readInventory }) {
const path = ` ${ ROOT } /dispatches/ ${ encodeURIComponent ( fixture . dispatchNo ) } `
const proof = context . createdResources . dispatchWorkflowReviews = { marker : context . marker , dispatchNo : fixture . dispatchNo ,
status : 'CHECKING' , commands : [], rejections : [], snapshots : {}, syntheticTestMaterials : true }
const request = async ( method , target , body , label , reject = false ) => {
if ( method !== 'GET' ) { proof . commands . push ({ method , path : target , body : clone ( body ) }); await context . checkpoint () }
const response = await context . request ( method , target , body )
if ( reject ) {
const denied = [ 200 , 409 ]. includes ( response . http ) && [ 'A0409' , '409' ]. includes ( String ( response . body ? . code ))
context . assert ( MODULE , label , response , denied , '' , OPTIONS ); assert ( denied )
proof . rejections . push ({ label , http : response . http , code : response . body . code , requestId : response . requestId , traceId : response . traceId })
return response
}
const data = context . apiSuccess ( MODULE , label , response , '' , OPTIONS )
assert . equal ( response . http , 200 ); assert . equal ( String ( response . body ? . code ), '200' ); assert ( data ); return data
}
const get = ( target , label ) => request ( 'GET' , target , undefined , label )
const snapshot = async label => {
const data = { main : await get ( path , label ), items : await get ( ` ${ path } /items` , label ), route : await get ( ` ${ path } /route` , label ), stock : [] }
for ( const member of fixture . members ) data . stock . push ( await readInventory ({ marker : context . marker , customerId : member . customerId , warehouseId : fixture . warehouseId }))
proof . snapshots [ label ] = data ; await context . checkpoint (); return data
}
const initial = await snapshot ( 'INITIAL_APPROVED_ZERO_PICK' )
assert . equal ( initial . main . stage , 'LOADING' ); assert . equal ( initial . main . reviewState , 'APPROVED' )
assert . equal ( Number ( initial . main . counts . pickedPackageCount ), 0 ); assert . equal ( Number ( initial . main . counts . shippedPackageCount ), 0 )
const key = label => ` ${ context . marker } -REV- ${ label } `
let result = await request ( 'POST' , ` ${ path } /return-to-review` , { recordVersion : initial . main . recordVersion , idempotencyKey : key ( 'RETURN' ), reason : 'TEST零扫码显式退回改货改车' }, 'Zero-pick explicit return' )
assert . equal ( result . stage , 'BEFORE_LOADING' )
let before = await snapshot ( 'RETURNED' )
assert . equal ( before . main . reviewState , 'DRAFT' )
await request ( 'POST' , ` ${ path } /submit-review` , { recordVersion : initial . main . recordVersion , idempotencyKey : key ( 'STALE' ), remark : context . marker }, 'Stale main version is rejected' , true )
assert . deepEqual ( await snapshot ( 'AFTER_STALE' ), before )
const submit = { recordVersion : before . main . recordVersion , idempotencyKey : key ( 'SUBMIT-REJECT-ROUND' ), remark : 'TEST第一重提轮次' }
result = await request ( 'POST' , ` ${ path } /submit-review` , submit , 'Submit dispatch rejection round' )
before = await snapshot ( 'SUBMITTED' )
const replay = await request ( 'POST' , ` ${ path } /submit-review` , submit , 'Original submit key replays' )
assert . equal ( replay . replayed , true ); assert . equal ( String ( replay . recordVersion ), String ( result . recordVersion ))
await request ( 'POST' , ` ${ path } /submit-review` , { ... submit , remark : 'TEST同键异载荷不得复用' }, 'Changed payload with original key is rejected' , true )
await request ( 'POST' , ` ${ path } /submit-review` , { ... submit , recordVersion : before . main . recordVersion , idempotencyKey : key ( 'DUPLICATE-SUBMIT' ) }, 'New-key duplicate submit is rejected' , true )
assert . deepEqual ( await snapshot ( 'AFTER_DUPLICATE_REJECTIONS' ), before )
result = await request ( 'POST' , ` ${ path } /dispatch-review` , { recordVersion : before . main . recordVersion , idempotencyKey : key ( 'DISPATCH-REJECT' ), decision : 'REJECTED' , reason : 'TEST调度驳回' }, 'Dispatch reviewer rejects' )
let detail = await get ( path , 'Read dispatch rejection' )
assert . equal ( detail . reviewState , 'DISPATCH_REJECTED' ); assert . equal ( detail . reviews . dispatchReview . status , 'REJECTED' )
result = await request ( 'POST' , ` ${ path } /submit-review` , { recordVersion : detail . recordVersion , idempotencyKey : key ( 'SUBMIT-LOADING-REJECT' ), remark : 'TEST驳回重提' }, 'Resubmit after dispatch rejection' )
result = await request ( 'POST' , ` ${ path } /dispatch-review` , { recordVersion : result . recordVersion , idempotencyKey : key ( 'DISPATCH-APPROVE' ), decision : 'APPROVED' , reason : 'TEST调度通过' }, 'Dispatch approves new round' )
result = await request ( 'POST' , ` ${ path } /loading-review` , { recordVersion : result . recordVersion , idempotencyKey : key ( 'LOADING-REJECT' ), decision : 'REJECTED' , reason : 'TEST理货驳回' }, 'Loading reviewer rejects' )
detail = await get ( path , 'Read loading rejection' )
assert . equal ( detail . reviewState , 'LOADING_REJECTED' ); assert . equal ( detail . reviews . loadingReview . status , 'REJECTED' )
const second = fixture . members [ 1 ]
result = await request ( 'POST' , ` ${ path } /items/ ${ encodeURIComponent ( second . dispatchItemId ) } /remove` , {
recordVersion : detail . recordVersion , idempotencyKey : key ( 'REMOVE-SECOND' ), reason : 'TEST零扫码先移出一票再真实重选' }, 'Remove only unpicked second member' )
const removed = await snapshot ( 'SECOND_REMOVED' )
assert . equal ( Number ( removed . main . counts . packageCount ), 1 ); assert . equal ( removed . items . items . length , 1 )
assert ( removed . route . stops . every ( stop => stop . destinationKey !== second . destination . key ))
const candidates = await get ( ` ${ ROOT } /dispatches/candidates? ${ new URLSearchParams ({ warehouseId : fixture . warehouseId , orderNo : second . incomingNo } )}` , 'Read freshly released source candidate' )
const choices = candidates . candidates ? . filter ( row => id ( row . customerId ) === second . customerId && id ( row . selection ? . warehousePalletId ) === second . palletId && row . palletNo === second . palletNo )
assert . equal ( choices ? . length , 1 ); const choice = choices [ 0 ]
assert ( choice . selection && choice . sourceVersion && choice . blockReasons ? . length === 0 && Number ( choice . availablePackages ) === 1 )
result = await request ( 'POST' , ` ${ path } /items` , { recordVersion : result . recordVersion , idempotencyKey : key ( 'ADD-SECOND' ),
cargo : [{ ... choice . selection , warehousePalletId : second . palletId , sourceVersion : choice . sourceVersion }], batchAllocations : [] }, 'Re-add exact released current source' )
const added = await snapshot ( 'SECOND_RESTORED' )
assert . equal ( added . items . items . length , 2 ); assert . equal ( Number ( added . main . counts . packageCount ), 2 )
const replacement = added . items . items . filter ( row => id ( row . customerId ) === second . customerId && id ( row . warehousePalletId ) === second . palletId )
assert . equal ( replacement . length , 1 ); second . dispatchItemId = id ( replacement [ 0 ]. dispatchItemId )
const nextPlate = `FT- ${ context . marker . slice ( - 12 ) } -R2`
const vehicle = await request ( 'POST' , ` ${ ROOT } /master-data/vehicles` , { plateNumber : nextPlate , model : 'Box Truck' , fuelConsumptionPer100Km : 12.5 ,
mileageKm : 100 , region : 'OVERSEAS' , ownership : 'OWNED' , status : 'IN_SERVICE' , maxPalletSpaces : 30 , maxLoadKg : 18000 , maxVolumeCbm : 90 ,
remark : ` ${ context . marker } TEST零扫码改派自有车辆` }, 'Create owned replacement TEST vehicle' )
const update = { recordVersion : result . recordVersion , idempotencyKey : key ( 'CHANGE-VEHICLE' ), vehicle : { mode : 'MASTER' , vehicleId : id ( vehicle . id ) },
driver : { mode : 'MASTER' , driverId : fixture . tripProof . driverId , evidence : { expectedTaskEndAt : fixture . tripProof . expectedDriverEndAt } },
plannedDepartureAt : new Date ( Date . now () + 3600000 ). toISOString (), estimatedArrivalAt : null , remark : ` ${ context . marker } TEST零扫码改车需两级重审` }
result = await request ( 'PUT' , path , update , 'Change resources before re-review' )
detail = await get ( path , 'Resource edit invalidates old approvals' )
assert . equal ( detail . reviewState , 'DRAFT' ); assert . equal ( id ( detail . vehicleInfo . vehicle . vehicleId ), id ( vehicle . id ))
assert . notEqual ( id ( vehicle . id ), fixture . tripProof . vehicleId )
for ( const [ action , suffix ] of [[ 'submit-review' , 'FINAL-SUBMIT' ], [ 'dispatch-review' , 'FINAL-DISPATCH' ], [ 'loading-review' , 'FINAL-LOADING' ]]) {
result = await request ( 'POST' , ` ${ path } / ${ action } ` , { recordVersion : result . recordVersion , idempotencyKey : key ( suffix ),
...( action === 'submit-review' ? { remark : 'TEST变更后重审' } : { decision : 'APPROVED' , reason : 'TEST新依据重审通过' }) }, `Final new round ${ action } ` )
}
const final = await snapshot ( 'REAPPROVED_ZERO_PICK' )
assert . equal ( final . main . stage , 'LOADING' ); assert . equal ( final . main . reviewState , 'APPROVED' )
assert ( Number ( final . main . reviews . dispatchReview . reviewRound ) > Number ( initial . main . reviews . dispatchReview . reviewRound ))
assert . equal ( final . main . reviews . dispatchReview . reviewRound , final . main . reviews . loadingReview . reviewRound )
assert . deepEqual ( final . stock , initial . stock , 'All zero-pick review and cargo/resource edits preserve complete inventory' )
const fleet = await get ( ` ${ ROOT } /fleet/trips/ ${ encodeURIComponent ( fixture . tripNo ) } ` , 'Read same main updated trip resources and route' )
assert . equal ( fleet . trip . tripNo , fixture . tripNo ); assert . equal ( id ( fleet . trip . vehicleId ), id ( vehicle . id ))
assert . equal ( fleet . dispatches . length , 1 ); assert . equal ( fleet . dispatches [ 0 ]. dispatchNo , fixture . dispatchNo )
Object . assign ( fixture . tripProof , { marker : nextPlate , vehicleId : id ( vehicle . id ), vehicle : final . main . vehicleInfo . vehicle ,
vehicleInfo : final . main . vehicleInfo , driver : final . main . vehicleInfo . driver , updateRequest : update , trip : fleet . trip ,
stops : fleet . trip . stops , fleetRecordVersion : fleet . trip . recordVersion , recordVersion : final . main . recordVersion })
proof . membersAfterEdit = clone ( fixture . members ); proof . finalVehicleInfo = clone ( final . main . vehicleInfo )
proof . status = 'VERIFIED' ; await context . checkpoint (); return proof
}
export async function verifyReviewExtension ( context , fixture , options ) {
await exerciseReviewLifecycle ( context , fixture , options )
return verifyDualDestination ( context , fixture , options )
}
2026-10-20 16:45:04 +08:00
export function assertConfirmedRelabelCancellation ( before , after , outbound , stockBefore , stockAfter ) {
assert . equal ( before . application . status , 'READY_FOR_OUTBOUND' )
assert ( before . application . customerConfirmedAt && before . application . outboundOrderNo )
assert . equal ( after . application . relabelNo , before . application . relabelNo )
assert . equal ( after . application . customerConfirmedAt , before . application . customerConfirmedAt )
assert . equal ( after . application . status , 'READY_FOR_OUTBOUND' )
assert . equal ( after . application . deliveryMethod , before . application . deliveryMethod )
assert . equal ( after . application . outboundOrderNo ?? null , null )
assert . equal ( after . application . fulfillmentStatus , 'WAIT_OUTBOUND_CREATION' )
assert . deepEqual ( after . items , before . items )
assert . equal ( outbound . outbound . status , 'CANCELLED' )
assert . equal ( outbound . outbound . outboundNo , before . application . outboundOrderNo )
assert . deepEqual ( stockAfter , stockBefore , 'Zero-scan cancellation preserves all completed relabel stock and immutable ledger rows' )
}
/** 独立第三客户三箱正式实收;不改变正常链两个客户的一箱库存基线。 */
export async function exerciseRelabelExtensions ( context , fixture , { readInventory }) {
const proof = context . createdResources . dispatchWorkflowRelabel = { marker : context . marker , status : 'PREPARING' ,
syntheticTestMaterials : true , hardwareVerified : false , physicalMeasurementVerified : false ,
scenarios : [], sharedDispatches : [], nonTruckRearrangement : 'NOT_TESTED_NO_FORMAL_PUBLIC_ENTRY' }
const key = label => ` ${ context . marker } -RL- ${ label } `
const api = async ( method , path , body , token ) => {
const response = await context . request ( method , path , body , token )
const data = context . apiSuccess ( MODULE , 'Formal relabel extension HTTP' , response , '' , OPTIONS )
assert . equal ( response . http , 200 ); assert . equal ( String ( response . body ? . code ), '200' ); return data
}
const png = Buffer . from ( 'iVBORw0KGgoAAAANSUhEUgAAAAEAAAABCAQAAAC1HAwCAAAAC0lEQVR42mP8/x8AAwMCAO+aH1kAAAAASUVORK5CYII=' , 'base64' )
const upload = async ( path , name ) => {
const response = await context . requestMultipart ( 'POST' , path , { name , type : 'image/png' , content : png }, fixture . pdaToken )
const data = context . apiSuccess ( MODULE , 'Upload explicitly synthetic TEST relabel evidence' , response , '' , OPTIONS )
assert . equal ( String ( response . body ? . code ), '200' ); assert ( data ? . fileId ); return id ( data . fileId )
}
const master = await api ( 'GET' , `/api/platform/v1/warehouse-admin/ ${ encodeURIComponent ( fixture . members [ 0 ]. destination . warehouseId ) } ?tenantId=0` )
const address = master . addresses ? . find ( row => row . addressLine1 )
assert ( address && master . warehouse ? . status === 'ACTIVE' )
const child = Object . create ( context )
child . marker = ` ${ context . marker } -RELABEL` ; child . options = { ... context . options , customerName : `TEST换标扩展- ${ context . marker . slice ( - 12 ) } ` }
child . context = {}; child . createdResources = {}
child . request = async (... args ) => context . request (... args )
await createQualifiedMerchant ( child , { publishEnvironment : false })
const credentials = child . context . merchantQualificationFixture
assert ( credentials ? . tenantId && credentials . marker === child . marker )
proof . customerId = id ( credentials . tenantId ); proof . qualification = clone ( child . createdResources . merchantQualificationFixture )
let session
try { session = await api ( 'POST' , '/api/auth/login' , { loginId : credentials . username , password : credentials . password }, '' ) }
finally { delete child . context . merchantQualificationFixture }
assert ( session ? . accessToken && session . refreshToken )
const merchantToken = session . accessToken , customerId = proof . customerId
const stock = () => readInventory ({ marker : context . marker , customerId , warehouseId : fixture . warehouseId })
try {
const profile = await api ( 'GET' , '/api/merchant/v1/iam/tenant/profile' , undefined , merchantToken )
assert . equal ( id ( profile . id ), customerId )
const skuCode = `RL- ${ context . marker . slice ( - 12 ) } `
const merchantRoot = '/api/merchant/v1/last-leg/warehouse-transfers'
const create = { sourceLastlegOrderIds : [], operatingWarehouseId : fixture . warehouseId ,
fbaWarehouseCode : master . warehouse . warehouseCode , destinationType : 'FBA' , destinationWarehouseId : id ( master . warehouse . id ),
destinationWarehouseCode : master . warehouse . warehouseCode , destinationWarehouseName : master . warehouse . warehouseName ,
deliveryAddress : [ address . countryCode , address . stateProvince , address . city , address . addressLine1 , address . postalCode ]. filter ( Boolean ). join ( ' ' ),
packageCount : 3 , volumeCbm : 3 , deliveryMethod : 'CUSTOMER_DELIVERY' , estimatedArrivalAt : new Date ( Date . now () + 3600000 ). toISOString (). slice ( 0 , 19 ),
needRelabel : false , remark : ` ${ context . marker } TEST三箱换标独立来货` , idempotencyKey : key ( 'RECEIPT-CREATE' ),
items : [{ skuCode , productNameZh : 'TEST三箱换标货物' , packageCount : 3 , quantity : 3 }] }
proof . receiptCreate = clone ( create ); await context . checkpoint ()
let incoming = await api ( 'POST' , merchantRoot , create , merchantToken )
proof . incomingNo = incoming . transferNo ; assert ( proof . incomingNo )
const merchantPath = ` ${ merchantRoot } / ${ encodeURIComponent ( proof . incomingNo ) } `
incoming = await api ( 'POST' , ` ${ merchantPath } /submit` , undefined , merchantToken )
incoming = await api ( 'POST' , ` ${ ROOT } /warehouse-transfers/ ${ encodeURIComponent ( proof . incomingNo ) } /review?customerId= ${ customerId } ` ,
{ decision : 'APPROVE' , remark : ` ${ context . marker } TEST三箱独立实收` })
assert . equal ( incoming . status , 'WAIT_PDA_RECEIVING' )
const pda = `/api/pda/v1/last-leg/warehouse-transfers/ ${ encodeURIComponent ( proof . incomingNo ) } `
const task = await api ( 'GET' , pda , undefined , fixture . pdaToken )
assert . equal ( Number ( task . packageCount ), 3 ); assert . equal ( task . items . length , 1 ); assert . equal ( task . cartons . length , 3 )
assert . equal ( task . items [ 0 ]. skuCode , skuCode ); assert . equal ( Number ( task . items [ 0 ]. quantity ), 3 )
assert . equal ( new Set ( task . cartons . map ( row => row . cartonMark )). size , 3 )
const draft = await api ( 'POST' , ` ${ pda } /pallets/drafts` , { idempotencyKey : key ( 'DRAFT' ) }, fixture . pdaToken )
assert . equal ( draft . pallets . length , 1 )
const palletPath = ` ${ pda } /pallets/ ${ encodeURIComponent ( draft . pallets [ 0 ]. id ) } `
const generated = await api ( 'POST' , ` ${ palletPath } /generate` , { idempotencyKey : key ( 'GENERATE' ) }, fixture . pdaToken )
const pallet = generated . pallets [ 0 ]; assert ( pallet . warehousePalletId && pallet . palletNo )
await api ( 'POST' , ` ${ palletPath } /bind` , { palletCode : pallet . palletNo , idempotencyKey : key ( 'BIND' ) }, fixture . pdaToken )
const counted = await api ( 'POST' , ` ${ pda } /cartons/count` , { forecastPackageCount : 3 , idempotencyKey : key ( 'COUNT' ),
boxes : task . cartons . map ( carton => ({ cartonMark : carton . cartonMark , transferItemId : id ( task . items [ 0 ]. id ), transferPalletId : id ( draft . pallets [ 0 ]. id ),
length : 100 , width : 100 , height : 100 , weight : 100 , items : [{ skuCode , quantity : 1 }] })) }, fixture . pdaToken )
assert . equal ( counted . cartons . length , 3 ); assert ( counted . cartons . every ( row => row . status === 'COUNTED' && Number ( row . quantity ) === 1 ))
assert . equal ( Number ( counted . pallets [ 0 ]. cartonCount ), 3 ); assert . equal ( Number ( counted . pallets [ 0 ]. grossWeightKg ), 300 )
const photoId = await upload ( ` ${ pda } /measurement-photos/files` , ` ${ context . marker } -TEST-three-boxes.png` )
incoming = await api ( 'POST' , ` ${ pda } /palletize` , { idempotencyKey : key ( 'PALLETIZE' ), photoFileIds : [ photoId ] }, fixture . pdaToken )
assert . equal ( incoming . status , 'WAIT_CUSTOMER_CONFIRMATION' )
incoming = await api ( 'POST' , ` ${ merchantPath } /confirm-measurement` , { idempotencyKey : key ( 'CONFIRM-RECEIPT' ), remark : context . marker }, merchantToken )
assert . equal ( incoming . status , 'READY_FOR_DISPATCH' )
proof . receipt = await stock ()
assert . equal ( proof . receipt . balances . length , 1 ); assert . equal ( proof . receipt . positions . length , 1 ); assert . equal ( proof . receipt . ledger . length , 1 )
assert . equal ( Number ( proof . receipt . balances [ 0 ]. availablePackages ), 3 )
assert . equal ( id ( proof . receipt . positions [ 0 ]. palletId ), id ( pallet . warehousePalletId ))
const candidates = await api ( 'GET' , `/api/merchant/v1/last-leg/relabel-applications/inventory-candidates?operationWarehouseId= ${ fixture . warehouseId } ` , undefined , merchantToken )
const sources = candidates . filter ( row => id ( row . inventoryBalanceId ) === id ( proof . receipt . balances [ 0 ]. id ) && row . inboundCode === proof . incomingNo )
assert . equal ( sources . length , 1 ); const source = sources [ 0 ]
const positions = source . positions . filter ( row => id ( row . positionId ) === id ( proof . receipt . positions [ 0 ]. id ))
assert . equal ( positions . length , 1 ); assert . equal ( Number ( positions [ 0 ]. availablePackages ), 3 )
const resolve = code => api ( 'POST' , '/api/pda/v1/last-leg/pallet-operations/pallets/resolve' , { code }, fixture . pdaToken )
let sharedTarget
for ( const [ index , method ] of [ 'TRUCK' , 'TRUCK' , 'PICKUP' ]. entries ()) {
const name = ` ${ method } - ${ index + 1 } ` , scenario = { name , operationType : 'CARTON_LABEL' , deliveryMethod : method }
proof . scenarios . push ( scenario )
const item = { sourceInventoryBalanceId : id ( source . inventoryBalanceId ), sourceInventoryPositionId : id ( positions [ 0 ]. positionId ),
sourceInboundOrderId : id ( source . inboundOrderId ), skuCode , productNameZh : 'TEST三箱换标货物' , totalPackageCount : 1 , notifiedPackageCount : 1 ,
shipmentNo : `TEST-RL- ${ context . marker . slice ( - 12 ) } - ${ index + 1 } ` , cartonMark : task . cartons [ index ]. cartonMark ,
fbaCartonMark : `TEST-NEW- ${ context . marker . slice ( - 12 ) } - ${ index + 1 } ` , destinationFbaWarehouseCode : master . warehouse . warehouseCode ,
operationType : 'CARTON_LABEL' , skuItems : [], labelRequirement : ` ${ context . marker } TEST换标成果` }
scenario . createBody = { customerOrderNo : `TEST- ${ context . marker . slice ( - 12 ) } - ${ name } ` , operationWarehouseId : fixture . warehouseId ,
labelDeadline : new Date ( Date . now () + 7 * 86400000 ). toISOString (). slice ( 0 , 19 ), contactName : 'TEST换标验收' , contactPhone : '19900000000' ,
remark : context . marker , idempotencyKey : key ( ` ${ name } -CREATE` ), deliveryMethod : method , printShippingLabel : false , items : [ item ] }
await context . checkpoint ()
const created = await api ( 'POST' , ` ${ ROOT } /relabel-applications?customerId= ${ customerId } ` , scenario . createBody )
scenario . relabelNo = created . application . relabelNo ; assert ( scenario . relabelNo )
assert . equal ( created . application . status , 'PENDING_REVIEW' )
assert ( created . application . lastlegOrderId )
const approved = await api ( 'PUT' , ` ${ ROOT } /orders/ ${ encodeURIComponent ( created . application . lastlegOrderId ) } /relabel-review?customerId= ${ customerId } ` ,
{ status : 'PENDING_PROCESSING' , remark : ` ${ context . marker } TEST正式审核后允许PDA作业` })
assert . equal ( approved . application . status , 'PENDING_PROCESSING' ); scenario . approved = clone ( approved )
const taskRoot = `/api/pda/v1/last-leg/relabel-tasks/ ${ encodeURIComponent ( scenario . relabelNo ) } `
let detail = await api ( 'POST' , ` ${ taskRoot } /claim` , { idempotencyKey : key ( ` ${ name } -CLAIM` ) }, fixture . pdaToken )
assert . equal ( detail . items . length , 1 )
detail = await api ( 'POST' , ` ${ taskRoot } /units` , { itemId : id ( detail . items [ 0 ]. itemId ), oldLabel : item . cartonMark , newLabel : item . fbaCartonMark ,
skuVerifications : [], expectedVersion : detail . version , idempotencyKey : key ( ` ${ name } -SCAN` ) }, fixture . pdaToken )
let target
if ( index === 1 ) target = sharedTarget
else target = await api ( 'POST' , '/api/pda/v1/last-leg/pallet-operations/relabel-pallet-label' , { relabelNo : scenario . relabelNo ,
idempotencyKey : key ( ` ${ name } -LABEL` ) }, fixture . pdaToken )
assert ( target ? . palletId && target . palletCode ); if ( index === 0 ) sharedTarget = target
const from = await resolve ( pallet . palletNo ), to = await resolve ( target . palletCode )
await api ( 'POST' , '/api/pda/v1/last-leg/pallet-operations/relabel-disassemble' , { relabelNo : scenario . relabelNo ,
idempotencyKey : key ( ` ${ name } -MOVE` ), source : { palletCode : from . palletCode , expectedVersion : from . palletVersion },
target : { palletCode : to . palletCode , expectedVersion : to . palletVersion }, cartonMarks : [ item . cartonMark ], reason : ` ${ context . marker } TEST实际箱移绑` }, fixture . pdaToken )
scenario . targets = [ clone ( target )]
const fileId = await upload ( ` ${ taskRoot } /photos/files` , ` ${ context . marker } - ${ name } -TEST.png` )
detail = await api ( 'POST' , ` ${ taskRoot } /complete` , { photoFileIds : [ fileId ], expectedVersion : detail . version ,
idempotencyKey : key ( ` ${ name } -COMPLETE` ), remark : context . marker }, fixture . pdaToken )
assert . equal ( detail . applicationStatus , 'CUSTOMER_CONFIRM_PENDING' )
const confirmed = await api ( 'POST' , `/api/merchant/v1/last-leg/relabel-applications/ ${ encodeURIComponent ( scenario . relabelNo ) } /confirm-completion` ,
{ idempotencyKey : key ( ` ${ name } -CONFIRM` ), remark : context . marker }, merchantToken )
assert . equal ( confirmed . application . status , 'READY_FOR_OUTBOUND' ); assert ( confirmed . application . customerConfirmedAt )
scenario . confirmed = clone ( confirmed ); scenario . items = clone ( confirmed . items ); scenario . relabelVerified = true
scenario . destinationFbaWarehouseCode = master . warehouse . warehouseCode
await context . checkpoint ()
}
proof . sharedPhysical = await resolve ( sharedTarget . palletCode )
assert . equal ( Number ( proof . sharedPhysical . totalPackages ), 2 )
const pickup = proof . scenarios [ 2 ], relabelPath = ` ${ ROOT } /relabel-applications/ ${ encodeURIComponent ( pickup . relabelNo ) } `
const before = await api ( 'GET' , relabelPath ), stockBefore = await stock ()
assert . equal ( before . application . fulfillmentStatus , 'OUTBOUND_CREATED' ); assert ( before . application . outboundOrderNo )
const outboundPath = ` ${ ROOT } /outbound-orders/ ${ encodeURIComponent ( before . application . outboundOrderNo ) } `
const outboundBefore = await api ( 'GET' , outboundPath )
assert ([ 'WAIT_PICK' , 'WAIT_OUTBOUND' ]. includes ( outboundBefore . outbound . status ))
proof . cancel = { status : 'INTENT' , before , stockBefore , outboundBefore , body : { reason : ` ${ context . marker } TEST已确认换标零扫码撤销本次出库安排` } }
await context . checkpoint ()
await api ( 'POST' , ` ${ outboundPath } /cancel` , proof . cancel . body )
const after = await api ( 'GET' , relabelPath ), outboundAfter = await api ( 'GET' , outboundPath ), stockAfter = await stock ()
Object . assign ( proof . cancel , { after , outboundAfter , stockAfter })
assertConfirmedRelabelCancellation ( before , after , outboundAfter , stockBefore , stockAfter )
proof . cancel . status = 'VERIFIED' ; await context . checkpoint ()
context . assert ( MODULE , 'Confirmed relabel zero-scan outbound cancellation preserves completed items, customer confirmation and all raw stock' , null , true , '' , OPTIONS )
await verifyRelabelSharedDispatches ( context , fixture , { readInventory }, proof )
return proof
} finally {
await api ( 'POST' , '/api/auth/logout' , { refreshToken : session . refreshToken }, merchantToken )
proof . ownedMerchantSessionClosed = true ; await context . checkpoint ()
}
}
export async function verifyRelabelSharedDispatches ( context , fixture , { readInventory }, proof ) {
const customerId = proof . customerId , truck = proof . scenarios . slice ( 0 , 2 )
const stock = () => readInventory ({ marker : context . marker , customerId , warehouseId : fixture . warehouseId })
const api = async ( method , path , body ) => {
const response = await context . request ( method , path , body )
const data = context . apiSuccess ( MODULE , 'Exact owned relabel shared physical selection' , response , '' , OPTIONS )
assert . equal ( String ( response . body ? . code ), '200' ); return data
}
assert . equal ( proof . cancel . status , 'VERIFIED' ); assert . equal ( truck . length , 2 )
assert ( truck . every ( row => row . relabelVerified && row . deliveryMethod === 'TRUCK' ))
proof . status = 'CHECKING_SHARED_NATIVE'
for ( const round of [ 1 , 2 ]) {
const byKey = new Map ()
for ( const scenario of truck ) {
const page = await api ( 'GET' , ` ${ ROOT } /dispatches/candidates? ${ new URLSearchParams ({ warehouseId : fixture . warehouseId ,
sourceStage : 'RELABEL' , orderNo : scenario . relabelNo } )}` )
assert . equal ( page . complete , true )
for ( const row of page . candidates ) {
assert ( truck . some ( item => item . relabelNo === row . selection ? . relabelNo ))
const prior = byKey . get ( row . sourceKey )
if ( prior ) assert . deepEqual ( prior , row )
byKey . set ( row . sourceKey , row )
}
}
const rows = [... byKey . values ()]
assert . equal ( rows . length , 2 ); assert . equal ( new Set ( rows . map ( row => id ( row . selection . warehousePalletId ))). size , 1 )
const createBody = nativeRelabelCreateBody ( ` ${ context . marker } -ROUND- ${ round } ` , truck , rows , customerId , fixture . warehouseId )
const run = { round , status : 'INTENT' , createBody , stockBefore : await stock () }; proof . sharedDispatches . push ( run ); await context . checkpoint ()
run . result = await api ( 'POST' , ` ${ ROOT } /dispatches` , createBody )
assert ( run . result . dispatchNo )
const path = ` ${ ROOT } /dispatches/ ${ encodeURIComponent ( run . result . dispatchNo ) } `
run . detail = await api ( 'GET' , path ); run . items = await api ( 'GET' , ` ${ path } /items` )
assert . equal ( Number ( run . detail . counts . packageCount ), 2 ); assert . equal ( Number ( run . detail . counts . palletCount ), 1 )
assert . equal ( Number ( run . detail . counts . pickedPackageCount ), 0 ); assert . equal ( run . items . items . length , 2 )
assert . equal ( new Set ( run . items . items . map ( row => id ( row . warehousePalletId ))). size , 1 )
assert ( run . items . items . every ( row => id ( row . customerId ) === customerId && Number ( row . scheduledPackages ) === 1 ))
run . cancelBody = { recordVersion : run . detail . recordVersion , idempotencyKey : ` ${ context . marker } -RL-SHARED- ${ round } -CANCEL` , reason : ` ${ context . marker } TEST零扫码验证后撤销安排保留换标成果` }
await context . checkpoint (); run . cancelled = await api ( 'POST' , ` ${ path } /cancel` , run . cancelBody )
assert . equal ( run . cancelled . status , 'CANCELLED' ); run . stockAfter = await stock (); assert . deepEqual ( run . stockAfter , run . stockBefore )
for ( const scenario of truck ) {
const current = await api ( 'GET' , ` ${ ROOT } /relabel-applications/ ${ encodeURIComponent ( scenario . relabelNo ) } ` )
assert . equal ( current . application . status , 'READY_FOR_OUTBOUND' ); assert . equal ( current . application . customerConfirmedAt , scenario . confirmed . application . customerConfirmedAt )
assert . deepEqual ( current . items , scenario . confirmed . items )
}
run . status = 'VERIFIED' ; await context . checkpoint ()
}
assert . notEqual ( proof . sharedDispatches [ 0 ]. result . dispatchNo , proof . sharedDispatches [ 1 ]. result . dispatchNo )
context . assert ( MODULE , 'Two confirmed applications share one actual pallet without double counting and can be natively rearranged after zero-pick cancellation' , null , true , '' , OPTIONS )
proof . status = 'VERIFIED_WITH_NON_TRUCK_REARRANGEMENT_GAP' ; await context . checkpoint (); return proof
}
export async function reconcileRelabelShared ( context , fixture , options , prior ) {
const proof = context . createdResources . dispatchWorkflowRelabel = clone ( prior . createdResources . dispatchWorkflowRelabel )
assert . equal ( proof . sharedDispatches . length , 0 ); assert . equal ( proof . cancel . status , 'VERIFIED' )
assert ( proof . scenarios . every ( row => row . relabelVerified === true ))
const read = async path => {
const response = await context . request ( 'GET' , path )
assert . equal ( String ( response . body ? . code ), '200' ); return response . body . data
}
const main = await read ( ` ${ ROOT } /dispatches/ ${ encodeURIComponent ( fixture . dispatchNo ) } ` )
const trip = await read ( ` ${ ROOT } /fleet/trips/ ${ encodeURIComponent ( fixture . tripNo ) } ` )
assert . equal ( main . stage , 'DELIVERED' ); assert . equal ( trip . trip . status , 'COMPLETED' )
assert ( trip . trip . vehicleRelease && trip . trip . driverRelease )
const currentStock = await options . readInventory ({ marker : context . marker , customerId : proof . customerId , warehouseId : fixture . warehouseId })
assert . deepEqual ( currentStock , proof . cancel . stockAfter )
for ( const scenario of proof . scenarios ) {
const current = await read ( ` ${ ROOT } /relabel-applications/ ${ encodeURIComponent ( scenario . relabelNo ) } ` )
assert . equal ( current . application . status , 'READY_FOR_OUTBOUND' ); assert . equal ( current . application . customerConfirmedAt , scenario . confirmed . application . customerConfirmedAt )
assert . deepEqual ( current . items , scenario . confirmed . items )
}
await verifyRelabelSharedDispatches ( context , fixture , options , proof )
context . createdResources . fleetCrossCustomer = clone ( prior . createdResources . fleetCrossCustomer )
return context . createdResources . fleetCrossCustomer
}
export async function verifyRelabelExtension ( context , fixture , options ) {
const verified = await verifyDualDestination ( context , fixture , options )
try { await exerciseRelabelExtensions ( context , fixture , options ) }
catch ( error ) {
if ( ! context . createdResources . fleetWriteBoundary && ! fixture . tripProof . closure ) await finishFleetTrip ( context , MODULE , fixture . tripProof )
throw error
}
return verified
}
2026-10-20 15:48:38 +08:00
/** Read-only terminal reconciliation after the known sequential-read completion race; never replay fulfillment. */
export async function reconcileReviewTerminal ( context , fixture , { readInventory }, prior ) {
assert . equal ( prior . success , false )
assert . equal ( prior . error , 'Native main/items/PDA retain both true customers and physical pallets' )
assert . equal ( prior . createdResources . dispatchWorkflowReviews . status , 'VERIFIED' )
assert . equal ( prior . createdResources . fleetWriteBoundary , undefined )
const old = prior . createdResources . fleetCrossCustomer , path = ` ${ ROOT } /dispatches/ ${ encodeURIComponent ( fixture . dispatchNo ) } `
assert . equal ( old . dispatchNo , fixture . dispatchNo ); assert . equal ( old . marker , context . marker )
assert ( old . requests . filter ( row => row . label === 'Attach receipt to independently complete this delivery' ). length === 2 )
assert ( old . requests . every ( row => row . status === 'RESPONDED' ))
const get = async target => {
const response = await context . request ( 'GET' , target )
const data = context . apiSuccess ( MODULE , 'Explicit terminal reconciliation read only' , response , '' , OPTIONS )
assert . equal ( response . http , 200 ); assert . equal ( String ( response . body ? . code ), '200' ); assert ( data ); return data
}
const current = { trip : await get ( ` ${ ROOT } /fleet/trips/ ${ encodeURIComponent ( fixture . tripNo ) } ` ), dispatch : await get ( path ),
items : await get ( ` ${ path } /items` ), task : null , members : [] }
const pda = await context . request ( 'GET' , `/api/pda/v1/last-leg/dispatches/ ${ encodeURIComponent ( fixture . dispatchNo ) } ` , undefined , fixture . pdaToken )
assert . equal ( pda . http , 200 ); assert . equal ( String ( pda . body ? . code ), '200' ); current . task = pda . body . data
assert . equal ( current . dispatch . stage , 'DELIVERED' ); assert . equal ( current . dispatch . status , 'ACTIVE' )
for ( const value of [ current . items , current . task ]) {
assert . equal ( value . dispatchNo , fixture . dispatchNo ); assert . equal ( value . stage , 'DELIVERED' )
assert . equal ( id ( value . recordVersion ), id ( current . dispatch . recordVersion )); assert . equal ( value . items . length , 2 )
}
for ( const field of [ 'packageCount' , 'pickedPackageCount' , 'shippedPackageCount' ]) assert . equal ( Number ( current . dispatch . counts [ field ]), 2 )
assert . equal ( current . trip . trip . tripNo , fixture . tripNo )
assert . equal ( id ( current . trip . trip . vehicleId ), fixture . tripProof . vehicleId ); assert . equal ( id ( current . trip . trip . driverId ), fixture . tripProof . driverId )
assert . equal ( current . trip . dispatches . length , 1 ); assert . equal ( current . trip . dispatches [ 0 ]. dispatchNo , fixture . dispatchNo )
const extension = context . createdResources . dispatchWorkflowExtensions = clone ( prior . createdResources . dispatchWorkflowExtensions )
extension . status = 'CHECKING' ; extension . explicitTerminalContinuation = true ; extension . priorFailurePreserved = true
const allFeeIds = new Set ()
for ( const [ index , member ] of fixture . members . entries ()) {
const oldMember = old . members [ index ]
assert . equal ( member . customerId , oldMember . customerId ); assert . equal ( member . dispatchItemId , oldMember . dispatchItemId )
for ( const rows of [ current . items . items , current . task . items ]) {
const matches = rows . filter ( row => id ( row . dispatchItemId ) === member . dispatchItemId )
assert . equal ( matches . length , 1 ); assert . equal ( id ( matches [ 0 ]. customerId ), member . customerId )
assert . equal ( matches [ 0 ]. palletNo , member . palletNo ); assert . equal ( Number ( matches [ 0 ]. pickedPackages ), 1 )
}
const suffix = new URLSearchParams ({ customerId : member . customerId })
const outbound = await get ( ` ${ ROOT } /outbound-orders/ ${ encodeURIComponent ( oldMember . outboundNo ) } ? ${ suffix } ` )
const delivery = await get ( ` ${ ROOT } /delivery-orders/ ${ encodeURIComponent ( oldMember . deliveryNo ) } ? ${ suffix } ` )
assert . equal ( outbound . outbound . dispatchNo , fixture . dispatchNo ); assert . equal ( outbound . outbound . status , 'OUTBOUNDED' )
assert . equal ( delivery . order . dispatchNo , fixture . dispatchNo ); assert . equal ( delivery . order . status , 'DELIVERED' )
assert . equal ( id ( delivery . order . customerId ), member . customerId ); assert . equal ( delivery . order . destinationWarehouseCode , member . destination . warehouseCode )
const expected = extension . expectedFees [ member . customerId ]; assert . equal ( delivery . fees . length , expected . length )
for ( const fee of delivery . fees ) {
assert ( ! allFeeIds . has ( id ( fee . id ))); allFeeIds . add ( id ( fee . id ))
assert ( expected . some ( row => row . feeType === fee . feeType && row . currency === fee . currency && row . cents === feeCents ( fee . amount )))
}
const stock = await readInventory ({ marker : context . marker , customerId : member . customerId , warehouseId : fixture . warehouseId })
assert . deepEqual ( stock , old . snapshots . BEFORE_SIGN_1 . members [ index ]. stock , 'Terminal completion must not change already shipped inventory' )
current . members . push ({ outbound , delivery , stock })
}
assert . deepEqual ( await get ( path ), current . dispatch , 'Terminal state changed during reconciliation' )
const verified = context . createdResources . fleetCrossCustomer = { marker : context . marker , dispatchNo : fixture . dispatchNo ,
tripNo : fixture . tripNo , warehouseId : fixture . warehouseId , members : clone ( old . members ), status : 'VERIFIED' , snapshots : { FINAL : current },
continuation : 'TERMINAL_READONLY_AFTER_CONFIRMED_SIGNATURES' , priorError : prior . error ,
gateProof : old . gateProof , midTransactionRollback : 'NOT_TESTED_NO_CONTROLLED_INJECTION' }
extension . status = 'VERIFIED' ; await context . checkpoint (); return verified
}