2026-10-03 07:42:18 +08:00
import test from 'node:test'
import assert from 'node:assert/strict'
import { createFleetCrossCustomerFixture , preparationContract } from './fleet-cross-customer-fixture.mjs'
import { fixtureContract } from './fleet-cross-customer-acceptance.mjs'
// Strict offline HTTP model executes the real qualification and prepareFleetTrip helpers too.
function model ( fault = '' ) {
const copy = value => structuredClone ( value ), calls = [], customers = [], transfers = [], dispatches = []
const options = { execute : true , warehouseId : '10' , destinationWarehouseId : '20' , pdaToken : 'caller-pda-secret' }
const trip = { trip : { tripNo : 'TRIP' , recordVersion : '0' , warehouseId : '10' , status : 'PLANNED' , maxPalletSpaces : 30 ,
vehiclePlate : 'TEST' , driverName : 'TEST' , driverPhone : '19900000000' }, dispatches : [] }
const origin = { warehouse : { id : '10' , tenantId : '0' , warehouseType : 'OVERSEAS' , status : 'ACTIVE' , warehouseCode : 'ORIGIN' , warehouseName : 'Origin' },
capabilities : [{ capabilityCode : 'RECEIVING' , enabled : fault !== 'disabled receiving' }] }
const destination = { warehouse : { id : '20' , tenantId : '0' , warehouseType : 'FBA' , status : 'ACTIVE' , warehouseCode : 'FBA' , warehouseName : 'Destination' },
addresses : [{ addressLine1 : 'TEST street' , countryCode : 'US' }] }
const response = data => ({ http : 200 , body : { code : '200' , data : copy ( data ) }, requestId : 'offline-request' , traceId : 'offline-trace' })
const buckets = { availablePackages : '1' , holdPackages : '0' , allocatedPackages : '0' , pickedPackages : '0' , receivingPackages : '0' }
const stock = transfer => {
const n = transfers . indexOf ( transfer ), scope = { customerId : transfer . customerId , overseasWarehouseId : '10' , inboundCode : transfer . transferNo }
const position = { ... scope , ... buckets , id : `P ${ n } ` , balanceId : `B ${ n } ` , palletId : `PAL ${ n } ` , palletNo : `PALLET ${ n } ` }
if ( fault === 'foreign position' ) position . customerId = 'OTHER'
return { balance : { ... scope , ... buckets , id : `B ${ n } ` , inTransitPackages : '0' , procurementPackages : '0' }, positions : [ position ],
ledger : [{ ... scope , id : `L ${ n } ` , balanceId : `B ${ n } ` , positionId : `P ${ n } ` , palletNo : `PALLET ${ n } ` , businessType : 'STOCK_IN' , businessOrderNo : transfer . transferNo ,
beforePackages : '0' , deltaPackages : '1' , afterPackages : '1' }] }
}
const cargo = () => trip . dispatches . map ( binding => {
const d = dispatches . find ( row => row . dispatchNo === binding . dispatchNo ), n = dispatches . indexOf ( d )
return { dispatchNo : d . dispatchNo , customerId : binding . customerId , warehousePalletId : `PAL ${ n } ` , palletNo : `PALLET ${ n } ` ,
destinationKey : 'FBA:FBA' , destinationLabel : 'FBA' , packages : 1 }
})
const parentFixture = { marker : 'PARENT' , tenantId : 'PARENT-ID' , password : 'parent-secret' }
const context = { marker : 'RUN-CROSS' , token : 'admin-secret' , options : { customerName : 'Parent customer' }, context : { merchantQualificationFixture : parentFixture },
createdResources : { merchantQualificationFixture : { marker : 'PARENT' } },
assert ( _module , label , _response , passed ) { assert . ok ( passed , label ) },
apiSuccess ( _module , label , result ) { assert . equal ( result . http , 200 , label ); assert . equal ( result . body . code , '200' , label ); return result . body . data },
async findAvailablePhone () { return `1990000000 ${ customers . length } ` },
async requestWithBody ( method , raw , form ) {
calls . push ({ method , raw , multipart : true }); assert . ok ( form instanceof FormData )
assert . match ( raw , /^\/api\/platform\/v1\/files\/qualifications\/\d+\/required-package\?/ )
return response ({})
},
async requestMultipart ( method , raw , file , token = this . token ) {
calls . push ({ method , raw , token , multipart : true }); assert . ok ( file . content . length > 0 )
if ( raw . includes ( '/license?' )) return response ({ id : 'DRIVER' , recordVersion : '1' , licenseFileId : 'LICENSE' })
assert . match ( raw , /\/warehouse-transfers\/IN[01]\/measurement-photos\/files$/ ); assert . equal ( token , options . pdaToken )
return response ({ fileId : 'PHOTO' , sizeBytes : file . content . length })
},
async request ( method , raw , body , token = this . token ) {
calls . push ({ method , raw , body : copy ( body ), token })
const url = new URL ( raw , 'https://offline.invalid' ), path = url . pathname
if ( path . startsWith ( '/api/platform/' )) assert . equal ( token , 'admin-secret' )
if ( path . startsWith ( '/api/pda/' )) assert . equal ( token , options . pdaToken )
if ( path . endsWith ( '/warehouse-admin/10' )) return response ( origin )
if ( path . endsWith ( '/warehouse-admin/20' )) return response ( destination )
if ( path === '/api/platform/v1/iam/tenants' ) {
const n = customers . length , id = fault === 'reused customer' ? '100' : String ( 100 + n )
const customer = { id , username : `user ${ n } ` , password : `fresh-secret- ${ n } ` , marker : body . internalRemark }
customers . push ( customer )
return response ({ tenant : { id }, administratorUsername : customer . username , temporaryPassword : customer . password })
}
if ( path . endsWith ( '/submit-review' )) return response ({ status : 'PENDING' })
if ( path === '/api/platform/v1/iam/reviews' ) return response ( customers . map ( row => ({ id : `REV ${ row . id } ` , tenantId : row . id })))
if ( path . endsWith ( '/dimension-initialization' )) return response ({ status : 'READY' })
if ( path . endsWith ( '/approve' )) return response ({})
if ( path . endsWith ( '/qualifications' )) return response ([ 'BUSINESS_LICENSE' , 'LEGAL_PERSON_ID' ]. map ( qualificationType => ({ qualificationType , status : 'APPROVED' })))
if ( path === '/api/auth/login' ) {
assert . equal ( token , '' )
const customer = customers . find ( row => row . username === body . loginId ); assert . equal ( body . password , customer . password )
return response ({ accessToken : `merchant- ${ customer . id } -secret` , refreshToken : `refresh- ${ customer . id } -secret` })
}
if ( path === '/api/auth/logout' ) {
assert . equal ( body . refreshToken , token . replace ( 'merchant-' , 'refresh-' ))
if ( fault === 'logout failure' && token . includes ( '100' )) throw new Error ( 'offline logout failure' )
return response ( null )
}
const merchant = customers . find ( row => token === `merchant- ${ row . id } -secret` )
if ( path === '/api/merchant/v1/iam/tenant/profile' ) return response ({ id : fault === 'foreign login' ? 'OTHER' : merchant . id })
if ( path === '/api/merchant/v1/last-leg/warehouse-transfers' ) {
assert . ok ( merchant ); assert . equal ( body . inboundCode , undefined ); assert . deepEqual ( body . sourceLastlegOrderIds , [])
assert . equal ( body . needRelabel , false ); assert . equal ( body . items . length , 1 ); assert . equal ( body . items [ 0 ]. packageCount , 1 )
const transferNo = `IN ${ transfers . length } `
const transfer = { transferNo , inboundCode : transferNo , customerId : merchant . id , operatingWarehouseId : body . operatingWarehouseId ,
status : 'DRAFT' , pallets : [] }
transfers . push ( transfer ); return response ( transfer )
}
if ( path . includes ( '/warehouse-transfers/' )) {
const transfer = transfers . find ( row => path . includes ( `/ ${ row . transferNo } ` )), n = transfers . indexOf ( transfer )
assert . ok ( transfer )
if ( path . startsWith ( '/api/merchant/' )) assert . equal ( transfer . customerId , merchant . id )
if ( path . endsWith ( '/submit' )) transfer . status = 'PENDING_REVIEW'
else if ( path . endsWith ( '/review' )) { assert . equal ( url . searchParams . get ( 'customerId' ), transfer . customerId ); assert . equal ( body . decision , 'APPROVE' ); transfer . status = 'WAIT_PDA_RECEIVING' }
else if ( path . endsWith ( '/pallets/drafts' )) transfer . pallets = [{ id : `DRAFT ${ n } ` , status : 'DRAFT' , cartonCount : body . cartonCount }]
else if ( path . endsWith ( '/generate' )) Object . assign ( transfer . pallets [ 0 ], { status : 'GENERATED' , warehousePalletId : `PAL ${ n } ` , palletNo : `PALLET ${ n } ` })
else if ( path . endsWith ( '/bind' )) { assert . equal ( body . palletCode , transfer . pallets [ 0 ]. palletNo ); transfer . pallets [ 0 ]. status = 'BOUND' }
else if ( path . endsWith ( '/palletize' )) { transfer . status = 'WAIT_CUSTOMER_CONFIRMATION' ; transfer . pallets [ 0 ]. status = 'COMPLETED' }
else if ( path . endsWith ( '/confirm-measurement' )) transfer . status = 'READY_FOR_DISPATCH'
else assert . equal ( method , 'GET' )
return response ( transfer )
}
if ( path . includes ( '/inventory/' )) {
const transfer = transfers . find ( row => row . customerId === url . searchParams . get ( 'customerId' )), facts = stock ( transfer )
if ( path . endsWith ( '/positions' )) return response ( facts . positions )
if ( /\/balances\/B[01]$/ . test ( path )) return response ( facts . balance )
// Match InventoryLedgerView: the public DTO has neither balanceId nor positionId.
if ( path . endsWith ( '/ledger' )) return response ({ records : facts . ledger . map (({ balanceId , positionId , ... view }) => view ), total : fault === 'truncated ledger' ? 2 : 1 })
assert . ok ( path . endsWith ( '/balances' )); return response ({ records : [ facts . balance ], total : 1 })
}
if ( path . endsWith ( '/dispatches/candidates' )) {
const n = transfers . findIndex ( row => row . transferNo === url . searchParams . get ( 'orderNo' )), transfer = transfers [ n ]
assert . equal ( transfer . status , 'READY_FOR_DISPATCH' )
return response ([{ customerId : fault === 'foreign candidate' ? 'OTHER' : transfer . customerId , overseasWarehouseId : '10' , inboundCode : transfer . transferNo ,
warehousePalletId : `PAL ${ n } ` , palletNo : `PALLET ${ n } ` , planPalletId : `PLAN ${ n } ` , sourceStage : 'RECEIVED' , packageCount : 1 }])
}
if ( path . endsWith ( '/drafts/containers' )) return response ({ id : `CONTAINER ${ dispatches . length } ` })
if ( path . endsWith ( '/draft-locks/lock' )) return response ( null )
if ( path . endsWith ( '/pre-arrange' )) {
const n = Number ( body . planPalletIds [ 0 ]. slice ( - 1 ))
const dispatch = { dispatchNo : `DS ${ n } ` , overseasWarehouseId : '10' , status : 'PENDING_DEPARTURE' , items : [{ warehousePalletId : `PAL ${ n } ` }],
packageCount : 1 , pickedPackageCount : 0 , shippedPackageCount : 0 }
dispatches . push ( dispatch ); return response ( dispatch )
}
if ( path . endsWith ( '/master-data/vehicles' )) return response ({ id : 'VEHICLE' })
if ( path . endsWith ( '/fleet/drivers' )) return response ({ id : 'DRIVER' , recordVersion : '0' })
if ( path . endsWith ( '/fleet/drivers/DRIVER/review' )) return response ({ reviewStatus : 'APPROVED' })
if ( path . endsWith ( '/fleet/trips' )) { Object . assign ( trip . trip , body ); return response ( trip ) }
if ( path . includes ( '/fleet/trips/TRIP' )) {
if ( path . endsWith ( '/loading' )) return response ({ recordVersion : trip . trip . recordVersion , cargo : fault === 'missing second cargo' ? cargo (). slice ( 0 , 1 ) : cargo () })
if ( method !== 'GET' ) {
assert . equal ( body . recordVersion , trip . trip . recordVersion )
trip . trip . recordVersion = String ( Number ( trip . trip . recordVersion ) + 1 )
}
if ( path . endsWith ( '/assignment' )) Object . assign ( trip . trip , { vehicleId : body . vehicleId , driverId : body . driverId })
else if ( path . endsWith ( '/dispatches/bind' )) trip . dispatches . push ({ dispatchNo : body . dispatchNo , customerId : transfers [ Number ( body . dispatchNo . slice ( - 1 ))]. customerId , review : { status : 'PENDING' } })
else if ( path . endsWith ( '/plan' )) trip . trip . stops = body . stops
else if ( path . endsWith ( '/review' )) trip . dispatches . find ( row => path . includes ( `/ ${ row . dispatchNo } /` )). review . status = 'APPROVED'
return response ( trip )
}
const n = Number ( path . match ( /(?:DS|OUT|DL)([01])/ )[ 1 ]), dispatch = dispatches [ n ]
const order = { customerId : transfers [ n ]. customerId , deliveryNo : `DL ${ n } ` , outboundNo : `OUT ${ n } ` , dispatchNo : dispatch . dispatchNo , status : 'WAIT_DELIVERY' }
if ( path . endsWith ( '/external-freight' )) return response ({ requiresDeliveryFee : true , quotes : [] })
if ( path . endsWith ( '/transfer-delivery' )) {
assert . equal ( body . fees . length , 1 ); assert . equal ( body . fees [ 0 ]. amount , 30 )
return response ({ dispatchNo : dispatch . dispatchNo , outboundNo : `OUT ${ n } ` , deliveries : [ order ], packageCount : 1 })
}
if ( path . includes ( '/delivery-orders/' )) { assert . equal ( url . searchParams . get ( 'customerId' ), transfers [ n ]. customerId ); return response ({ order : fault === 'foreign delivery' ? { ... order , customerId : 'OTHER' } : order }) }
if ( path . includes ( '/outbound-orders/' )) return response ({ outbound : { outboundNo : `OUT ${ n } ` , dispatchNo : dispatch . dispatchNo , overseasWarehouseId : '10' , status : 'WAIT_OUTBOUND' }, deliveries : [ order ] })
if ( path . endsWith ( `/dispatches/ ${ dispatch . dispatchNo } ` )) return response ( dispatch )
throw new Error ( `Unexpected offline request ${ method } ${ path } ` )
},
}
options . readInventory = async scope => {
assert . equal ( scope . marker , context . marker ); assert . equal ( scope . warehouseId , options . warehouseId )
const facts = stock ( transfers . find ( row => row . customerId === scope . customerId ))
const raw = { scope : { ... scope , coverage : 'CUSTOMER_WAREHOUSE_COMPLETE' }, balances : [ facts . balance ], positions : facts . positions , ledger : facts . ledger }
2026-10-03 07:44:17 +08:00
// The physical position table links to its inbound through balance_id; only the public joined DTO has inboundCode.
for ( const position of raw . positions ) delete position . inboundCode
2026-10-03 07:42:18 +08:00
if ( fault === 'wrong raw scope' ) raw . scope . marker = 'OLD'
if ( fault === 'wrong raw ledger position' ) raw . ledger [ 0 ]. positionId = 'OTHER'
if ( fault === 'wrong raw ledger balance' ) raw . ledger [ 0 ]. balanceId = 'OTHER'
return copy ( raw )
}
return { context , options , calls , customers , transfers , trip , parentFixture }
}
test ( 'default preparation is network-free and exposes caller-owned lifecycle' , async () => {
assert . equal ( await createFleetCrossCustomerFixture (), preparationContract )
assert . equal ( preparationContract . stage , fixtureContract . stage )
})
test ( 'real helpers build independent current-run customers, receipts and one complete shared trip without picking' , async () => {
const m = model (), oldEnvironment = { username : process . env . MERCHANT_TEST_USERNAME , password : process . env . MERCHANT_TEST_PASSWORD }
const fixture = await createFleetCrossCustomerFixture ( m . context , m . options )
assert . equal ( fixture . stage , fixtureContract . stage )
assert . equal ( fixture . members . length , 2 ); assert . notEqual ( fixture . members [ 0 ]. customerId , fixture . members [ 1 ]. customerId )
assert . ok ( fixture . members . every ( row => row . receiptVerified && row . marker === m . context . marker && row . positionId && row . balanceId ))
assert . equal ( m . context . context . merchantQualificationFixture , m . parentFixture )
assert . deepEqual ( m . context . createdResources . merchantQualificationFixture , { marker : 'PARENT' })
assert . equal ( m . context . options . customerName , 'Parent customer' )
assert . deepEqual ({ username : process . env . MERCHANT_TEST_USERNAME , password : process . env . MERCHANT_TEST_PASSWORD }, oldEnvironment )
assert . notEqual ( m . customers [ 0 ]. marker , m . customers [ 1 ]. marker )
const proof = m . context . createdResources . fleetCrossCustomerFixture
assert . equal ( proof . status , 'READY' ); assert . deepEqual ( proof . sessionCleanup . map ( row => row . status ), [ 'LOGGED_OUT' , 'LOGGED_OUT' ])
assert . equal ( m . trip . dispatches . length , 2 ); assert . deepEqual ( m . trip . trip . stops . map ( row => row . destinationKey ). filter ( Boolean ), [ 'FBA:FBA' ])
assert . equal ( m . calls . filter ( row => row . raw . endsWith ( '/plan' )). length , 2 )
assert . equal ( m . calls . filter ( row => row . raw . endsWith ( '/transfer-delivery' )). length , 2 )
assert . ok ( ! m . calls . some ( row => / \ / pick$ | \ / depart$ | \ / bookings | \ / capacity - plan / . test ( row . raw )))
const persisted = JSON . stringify ( m . context . createdResources )
for ( const secret of [ 'caller-pda-secret' , 'admin-secret' , 'fresh-secret-' , 'merchant-100-secret' , 'refresh-100-secret' ]) assert . ok ( ! persisted . includes ( secret ))
})
for ( const fault of [ 'disabled receiving' , 'reused customer' , 'foreign login' , 'foreign position' , 'truncated ledger' , 'wrong raw scope' , 'wrong raw ledger position' , 'wrong raw ledger balance' , 'foreign candidate' , 'missing second cargo' , 'foreign delivery' , 'logout failure' ]) {
test ( `fail closed, retain partial resources and clean owned sessions: ${ fault } ` , async () => {
const m = model ( fault )
await assert . rejects ( createFleetCrossCustomerFixture ( m . context , m . options ))
const proof = m . context . createdResources . fleetCrossCustomerFixture
assert . equal ( proof . status , 'FAILED' )
const logins = m . calls . filter ( row => row . raw === '/api/auth/login' ), logouts = m . calls . filter ( row => row . raw === '/api/auth/logout' )
assert . equal ( logouts . length , logins . length )
assert . ok ( ! m . calls . some ( row => row . method === 'DELETE' || row . raw . includes ( '/users/' )))
if ( fault === 'disabled receiving' ) assert . equal ( m . calls . filter ( row => row . method !== 'GET' ). length , 0 )
const previousCount = m . calls . length
await assert . rejects ( createFleetCrossCustomerFixture ( m . context , m . options ), /already started/ )
assert . equal ( m . calls . length , previousCount )
})
}
test ( 'missing explicit warehouse or caller PDA fails before reading or writing APIs' , async () => {
for ( const field of [ 'warehouseId' , 'destinationWarehouseId' , 'pdaToken' , 'readInventory' ]) {
const m = model (); delete m . options [ field ]
await assert . rejects ( createFleetCrossCustomerFixture ( m . context , m . options ), /Missing/ )
assert . equal ( m . calls . length , 0 )
}
})