2026-08-13 16:14:54 +08:00
import assert from 'node:assert/strict'
import { readFileSync } from 'node:fs'
import test from 'node:test'
2026-08-14 19:59:26 +08:00
import { oceanBookingDraft , oceanBookingDraftWithoutCustomer , publishPlatformLogisticsChannels , resolveOceanBookingMaster } from './ocean-booking-fixture.mjs'
2026-08-13 16:14:54 +08:00
test ( 'ocean booking fixture resolves the current inbound customer channel and warehouse' , async () => {
const steps = []
const context = {
marker : 'ADMIN-RUN-1' ,
createdResources : { orchestrationFixtures : { phase3Container : {
marker : 'ADMIN-RUN-1' , tenantId : '21' , inboundId : '31' ,
} } },
request : async () => ({ body : { code : '200' , data : { summary : {
tenantId : 21 , customerName : 'IT 测试客户' , channelCode : 'SEA-US' , channelName : '美西海运' ,
warehouseId : 41 , warehouseName : '深圳仓' ,
} } } }),
addStep : (... args ) => steps . push ( args ),
}
const master = await resolveOceanBookingMaster ( context , 'test' )
assert . deepEqual ( master , { customerId : '21' , customerName : 'IT 测试客户' , channelCode : 'SEA-US' ,
channelName : '美西海运' , warehouseId : '41' , warehouseName : '深圳仓' })
assert . equal ( steps . length , 0 )
})
test ( 'ocean booking fixture blocks creation instead of inventing missing master data' , async () => {
const steps = []
const context = { marker : 'ADMIN-RUN-2' , createdResources : {}, addStep : (... args ) => steps . push ( args ) }
assert . equal ( await resolveOceanBookingMaster ( context , 'test' ), null )
assert . equal ( steps . length , 1 )
assert . match ( steps [ 0 ][ 4 ], /missing=customerId,customerName,channelCode,channelName,warehouseId,warehouseName/ )
})
2026-08-15 09:35:52 +08:00
test ( 'shared booking data omits server-owned channelName and includes offline-confirmed transport snapshots' , () => {
2026-08-13 16:14:54 +08:00
const draft = oceanBookingDraft ({ marker : 'ADMIN-RUN-3' }, '3' , {
customerId : '21' , customerName : 'IT 测试客户' , channelCode : 'SEA-US' , channelName : '美西海运' ,
warehouseId : '41' , warehouseName : '深圳仓' ,
})
2026-08-14 18:50:07 +08:00
for ( const key of [ 'customerId' , 'customerName' , 'channelCode' , 'warehouseId' , 'warehouseName' ]) {
2026-08-13 16:14:54 +08:00
assert . ok ( draft [ key ], ` ${ key } is required` )
}
2026-08-14 18:50:07 +08:00
assert . equal ( draft . channelName , undefined )
2026-08-15 11:05:58 +08:00
assert . equal ( draft . shippingCompanyId , null )
assert . equal ( draft . shippingCompanyName , null )
assert . equal ( draft . bookingAgentId , null )
assert . equal ( draft . bookingAgentName , null )
2026-08-14 19:59:26 +08:00
assert . equal ( draft . serviceTerm , 'CY-CY' )
assert . equal ( draft . freightTerm , 'NINGBO' )
2026-08-13 16:14:54 +08:00
assert . equal ( draft . containerRequirements [ 0 ]. capacityCbm , 68 )
assert . equal ( draft . containerRequirements [ 0 ]. maxWeightKg , 28000 )
2026-08-15 09:35:52 +08:00
assert . equal ( draft . billOfLadingNo , 'BL-3' )
assert . equal ( draft . vesselName , '验收船-3' )
assert . equal ( draft . voyageNo , 'V-3' )
assert . equal ( draft . estimatedSailingAt , '2026-09-01T08:00:00' )
assert . equal ( draft . estimatedArrivalAt , '2026-09-20T08:00:00' )
2026-08-14 19:59:26 +08:00
const fixtureSource = readFileSync ( new URL ( './ocean-booking-fixture.mjs' , import . meta . url ), 'utf8' )
const builderSource = fixtureSource . slice ( fixtureSource . indexOf ( 'export function oceanBookingDraft' ), fixtureSource . indexOf ( 'export function oceanBookingDraftWithoutCustomer' ))
for ( const key of [ 'billOfLadingNo' , 'vesselName' , 'voyageNo' , 'estimatedSailingAt' , 'estimatedArrivalAt' ]) {
2026-08-15 09:35:52 +08:00
assert . match ( builderSource , new RegExp ( ` ${ key } \\s*:` ), ` ${ key } must be emitted by the confirmed booking builder` )
2026-08-14 19:59:26 +08:00
}
})
2026-08-15 11:05:58 +08:00
test ( 'shared booking data never sends supplier names without valid supplier ids' , () => {
const draft = oceanBookingDraft ({ marker : 'ADMIN-RUN-SUPPLIER' }, 'supplier' , {
customerId : '21' , customerName : 'IT 测试客户' , channelCode : 'SEA-US' , channelName : '美西海运' ,
warehouseId : '41' , warehouseName : '深圳仓' ,
}, { shippingCompanyName : '无来源船公司' , bookingAgentName : '无来源代理' })
assert . equal ( draft . shippingCompanyName , null )
assert . equal ( draft . bookingAgentName , null )
})
2026-08-14 19:59:26 +08:00
test ( 'shared booking draft can omit customer identity without inventing placeholders' , () => {
const draft = oceanBookingDraftWithoutCustomer ({ marker : 'ADMIN-RUN-OPTIONAL' }, 'optional' , {
customerId : '21' , customerName : '不应写入' , channelCode : 'SEA-US' , channelName : '美西海运' ,
warehouseId : '41' , warehouseName : '深圳仓' ,
})
assert . equal ( Object . hasOwn ( draft , 'customerId' ), false )
assert . equal ( Object . hasOwn ( draft , 'customerName' ), false )
assert . equal ( draft . channelCode , 'SEA-US' )
assert . equal ( draft . warehouseId , '41' )
assert . equal ( draft . containerRequirements [ 0 ]. capacityCbm , 68 )
assert . equal ( draft . containerRequirements [ 0 ]. maxWeightKg , 28000 )
2026-08-13 16:14:54 +08:00
})
2026-08-14 18:50:07 +08:00
test ( 'platform channel producer queries the selected origin warehouse and publishes current-marker resources' , async () => {
const calls = []
const context = {
marker : 'ADMIN-RUN-CHANNELS' , createdResources : {},
request : async ( method , path ) => {
calls . push ({ method , path })
return { body : { code : '200' , data : [
{ channelCode : 'SEA-A' , channelName : '海运 A' },
{ channelCode : 'SEA-B' , channelName : '海运 B' },
] } }
},
apiSuccess : ( _module , _name , response ) => response . body . data ,
assert : ( _module , _name , _response , condition ) => assert . equal ( condition , true ),
}
const catalog = await publishPlatformLogisticsChannels ( context , '41' , 'test' )
assert . deepEqual ( calls , [{ method : 'GET' , path : '/api/platform/v1/ocean-bookings/channels?originWarehouseId=41' }])
assert . equal ( catalog . marker , context . marker )
assert . equal ( catalog . originWarehouseId , '41' )
assert . deepEqual ( catalog . channels . map (( value ) => value . channelCode ), [ 'SEA-A' , 'SEA-B' ])
assert . deepEqual ( context . createdResources . platformLogisticsChannels , catalog )
})
test ( 'all direct ocean booking payload builders omit channelName under the frozen DraftData contract' , () => {
const regions = [
[ 'manual-state-loading-pda.mjs' , 'const draft = {' , 'const createResponse' ],
[ 'manual-state-ocean-trucking.mjs' , 'function oceanPayload' , 'async function createOcean' ],
[ 'phase3-loading.mjs' , 'const bookingDraft = {' , 'const bookingCreate' ],
[ 'phase3-order-splitting.mjs' , 'function bookingPayload' , 'async function loadOneFragment' ],
[ 'phase3-container-loading.mjs' , 'const bookingDraft = {' , "context.assert(MODULE, '订舱渠道与仓库快照完整'" ],
]
for ( const [ name , start , end ] of regions ) {
const value = readFileSync ( new URL ( name , import . meta . url ), 'utf8' )
const region = value . slice ( value . indexOf ( start ), value . indexOf ( end ))
assert . doesNotMatch ( region , /channelName\s*:/ , name )
2026-08-15 09:35:52 +08:00
for ( const field of [ 'billOfLadingNo' , 'vesselName' , 'voyageNo' , 'estimatedSailingAt' , 'estimatedArrivalAt' ]) {
assert . match ( region , new RegExp ( ` ${ field } \\s*:` ), ` ${ name } must register ${ field } ` )
}
2026-08-14 18:50:07 +08:00
}
const container = readFileSync ( new URL ( 'phase3-container-loading.mjs' , import . meta . url ), 'utf8' )
assert . doesNotMatch ( container , /data:\s*\{[^}]*channelName\s*:/s)
})
2026-08-15 09:35:52 +08:00
test ( 'ocean booking producers register CONFIRMED data directly without draft submit or confirm transitions' , () => {
for ( const name of [ 'ocean-bookings.mjs' , 'phase3-loading.mjs' , 'phase3-container-loading.mjs' ,
'phase3-order-splitting.mjs' , 'manual-state-loading-pda.mjs' , 'manual-state-ocean-trucking.mjs' ]) {
const source = readFileSync ( new URL ( name , import . meta . url ), 'utf8' )
assert . doesNotMatch ( source , /ocean-bookings\/[^`'"\\s]+\/(?:submit|confirm)|booking-(?:submit|confirm)/ , name )
}
})
2026-08-13 16:14:54 +08:00
test ( 'all independent ocean booking producers use the shared master-data contract' , () => {
for ( const name of [ 'ocean-bookings.mjs' , 'phase3-port-schedules.mjs' , 'phase3-carrier-schedule-queries.mjs' ]) {
const source = readFileSync ( new URL ( name , import . meta . url ), 'utf8' )
assert . match ( source , /resolveOceanBookingMaster\(context, MODULE\)/ , name )
assert . match ( source , /oceanBookingDraft\(context, suffix, master/ , name )
}
const container = readFileSync ( new URL ( 'phase3-container-loading.mjs' , import . meta . url ), 'utf8' )
2026-08-14 18:50:07 +08:00
for ( const snippet of [ 'channelCode' , 'publishPlatformLogisticsChannels' , 'warehouseId' , 'warehouseName' ,
2026-08-13 16:14:54 +08:00
'capacityCbm: 100' , 'maxWeightKg: 100000' ]) assert . match ( container , new RegExp ( snippet ), snippet )
})