2026-09-15 12:07:43 +08:00
import { createHash } from 'node:crypto'
2026-08-08 10:11:04 +08:00
const MODULE = 'orders'
const CONTRACT_OPTIONS = { scenario : 'isolated-order-chain' , deploymentBlocker : true }
const BOXES = [
{ boxType : 'SMALL' , lengthCm : 30 , widthCm : 20 , heightCm : 20 },
{ boxType : 'MEDIUM' , lengthCm : 40 , widthCm : 30 , heightCm : 30 },
{ boxType : 'LARGE' , lengthCm : 50 , widthCm : 40 , heightCm : 40 },
{ boxType : 'EXTRA_LARGE' , lengthCm : 60 , widthCm : 50 , heightCm : 50 },
]
const MEASURABLE_STATUSES = new Set ([ 'WAIT_MEASURE' , 'MEASURING' , 'MEASURE_REJECTED' ])
2026-08-13 19:00:33 +08:00
const PRICING_PROBE_CONCURRENCY = 6
const ORDER_ESTIMATE_WEIGHT_KG = BOXES . reduce (( total , _box , index ) => total + 10 + index , 0 )
const ORDER_ESTIMATE_VOLUME_CBM = BOXES . reduce (( total , box ) =>
total + box . lengthCm * box . widthCm * box . heightCm / 1_000_000 , 0 )
2026-08-08 10:11:04 +08:00
function id ( value ) { return value == null ? '' : String ( value ) }
function records ( value ) { return Array . isArray ( value ? . records ) ? value . records : [] }
function pageOk ( value ) { return Array . isArray ( value ? . records ) && Number . isFinite ( Number ( value ? . total )) }
2026-08-20 15:20:05 +08:00
function success ( response ) { return Boolean ( response ) && response . http === 200 && String ( response . body ? . code ) === '200' }
2026-08-08 10:11:04 +08:00
function sleep ( milliseconds ) { return new Promise (( resolve ) => setTimeout ( resolve , milliseconds )) }
2026-09-24 03:41:12 +08:00
async function requestTransportRetry ( context , method , path , body , token , extraHeaders = {}) {
2026-09-23 22:00:00 +08:00
for ( let attempt = 1 ; attempt <= 3 ; attempt += 1 ) {
2026-09-24 03:41:12 +08:00
const response = await request ( context , method , path , body , token , extraHeaders )
2026-09-23 22:00:00 +08:00
if ( response ? . http !== 0 || attempt === 3 ) return response
await sleep ( 500 * attempt )
}
}
2026-08-27 09:21:01 +08:00
function rotate ( values , offset ) {
if ( ! Array . isArray ( values ) || values . length < 2 ) return Array . isArray ( values ) ? [... values ] : []
const shift = Math . abs ( Number ( offset ) || 0 ) % values . length
return [... values . slice ( shift ), ... values . slice ( 0 , shift )]
}
2026-08-08 10:11:04 +08:00
function key ( context , operation ) { return ` ${ context . marker } - ${ operation } ` . slice ( 0 , 128 ) }
2026-10-02 23:14:24 +08:00
export function freezeOrderCommand ( detail , idempotencyKey , payload = {}) {
const version = detail ? . order ? . version
if ( ! Number . isSafeInteger ( version ) || version < 0 || ! idempotencyKey ) throw new Error ( '订单首次详情版本或幂等键缺失' )
const command = structuredClone ({ body : { ... payload , expectedVersion : version }, headers : { 'Idempotency-Key' : idempotencyKey } })
const freeze = value => { if ( value && typeof value === 'object' ) { Object . values ( value ). forEach ( freeze ); Object . freeze ( value ) } }
freeze ( command )
return command
}
export async function verifyPlatformDraftEdit ( context , tenantId , orderId , merchantToken , payload ) {
const path = `/api/platform/v1/orders/first-leg/ ${ encodeURIComponent ( orderId ) } ?tenantId= ${ encodeURIComponent ( tenantId ) } `
const read = async label => context . apiSuccess ( MODULE , label , await context . request ( 'GET' , path ))
const before = await read ( 'Admin 编辑前冻结客户订单版本' )
if ( id ( before ? . order ? . tenantId ) !== id ( tenantId ) || id ( before ? . order ? . id ) !== id ( orderId ) || before . order . status !== 'DRAFT' ) throw new Error ( '平台编辑客户/草稿身份不符' )
const editedPayload = structuredClone ( payload )
editedPayload . customerRemark = ` ${ payload . customerRemark } 平台编辑验收`
const command = freezeOrderCommand ( before , key ( context , 'platform-draft-edit' ), { draft : editedPayload })
context . createdResources . orders . platformEditCommand = command
const send = ( body , headers , token = context . token ) => context . request ( 'PUT' , path , body , token , headers )
const unauthorized = await send ( command . body , command . headers , merchantToken )
context . assert ( MODULE , 'Client不得调用Platform编辑接口' , unauthorized ,
[ 200 , 403 ]. includes ( unauthorized . http ) && String ( unauthorized . body ? . code ) === 'A0403' , 'merchant-token/platform-route' , CONTRACT_OPTIONS )
const afterDenied = await read ( '平台编辑越权拒绝后回查' )
context . assert ( MODULE , '越权平台编辑后完整事实不变' , null , orderPersistenceFacts ( before ) === orderPersistenceFacts ( afterDenied ), 'no-drift' , CONTRACT_OPTIONS )
context . apiSuccess ( MODULE , 'Admin按客户范围版本化编辑草稿' , await send ( command . body , command . headers ))
const edited = await read ( '平台编辑后权威详情' )
context . assert ( MODULE , '平台编辑保持身份且版本与UPDATE历史递增' , null ,
id ( edited ? . order ? . id ) === id ( orderId ) && id ( edited . order . tenantId ) === id ( tenantId ) && edited . order . status === 'DRAFT'
&& edited . order . version > before . order . version && edited . order . customerRemark === editedPayload . customerRemark
&& edited . statusHistory . length === before . statusHistory . length + 1 , 'versioned-edit' , CONTRACT_OPTIONS )
context . apiSuccess ( MODULE , 'Platform编辑原键原版本重放成功' , await send ( command . body , command . headers ))
for ( const [ label , body , headers ] of [
[ 'Platform编辑原键异载荷冲突' , {... command . body , draft : {... editedPayload , customerRemark : ` ${ editedPayload . customerRemark } -conflict` }}, command . headers ],
[ 'Platform编辑旧页面版本冲突' , command . body , { 'Idempotency-Key' : key ( context , 'platform-edit-stale' )}],
]) {
const rejected = await send ( body , headers )
context . assert ( MODULE , label , rejected , rejected . http === 200 && String ( rejected . body ? . code ) === 'A0409' , 'exact-conflict' , CONTRACT_OPTIONS )
}
const stable = await read ( 'Platform重放及冲突后完整事实' )
context . assert ( MODULE , 'Platform重放与拒绝不重复写入' , null , orderPersistenceFacts ( stable ) === orderPersistenceFacts ( edited ), 'order/cargo/parties/history' , CONTRACT_OPTIONS )
return stable
}
2026-10-02 23:37:38 +08:00
export async function verifyPlatformSubmitCancel ( context , tenantId , payload , policy ) {
const base = '/api/platform/v1/orders/first-leg'
const scope = `tenantId= ${ encodeURIComponent ( tenantId ) } `
const proofs = context . createdResources . orders . platformLifecycle = []
for ( const action of [ 'submit' , 'cancel' ]) {
const draftPayload = { ... structuredClone ( payload ), ... fixtureInboundCode ( policy , ` ${ context . marker } -PLATFORM- ${ action } ` ),
customerRemark : ` ${ context . marker } Platform ${ action } independent draft` }
const created = await context . request ( 'POST' , ` ${ base } ? ${ scope } ` , draftPayload , context . token ,
{ 'Idempotency-Key' : key ( context , `platform- ${ action } -create` ) })
const orderId = id ( context . apiSuccess ( MODULE , `Platform ${ action } 创建本轮独立草稿` , created ))
if ( ! orderId ) throw new Error ( `Platform ${ action } fixture order missing` )
const path = ` ${ base } / ${ encodeURIComponent ( orderId ) } ? ${ scope } `
const mutationPath = ` ${ base } / ${ encodeURIComponent ( orderId ) } / ${ action } ? ${ scope } `
const proof = { action , tenantId , orderId , commands : [], checks : [] }
proofs . push ( proof )
const assertFact = ( label , response , passed , evidence ) => {
context . assert ( MODULE , `Platform ${ action } ${ label } ` , response , passed , evidence , CONTRACT_OPTIONS )
if ( ! passed ) throw new Error ( `Platform ${ action } ${ label } ` )
}
const read = async label => {
const response = await context . request ( 'GET' , path )
const detail = context . apiSuccess ( MODULE , `Platform ${ action } ${ label } ` , response )
assertFact ( '详情身份与完整事实集合' , response , id ( detail ? . order ? . id ) === orderId && id ( detail ? . order ? . tenantId ) === id ( tenantId )
&& Number . isSafeInteger ( detail . order . version ) && detail . order . version >= 0
&& [ 'cargoes' , 'parties' , 'internalNotes' , 'statusHistory' ]. every ( field => Array . isArray ( detail [ field ])), 'exact-customer/order/version/aggregate' )
return detail
}
const same = ( label , before , after ) => assertFact ( label , null , orderPersistenceFacts ( before ) === orderPersistenceFacts ( after ), 'state/version/cargo/parties/notes/history unchanged' )
const original = await read ( '冻结首次草稿版本' )
assertFact ( '取消与提交前置仅为未履约草稿' , created , original . order . status === 'DRAFT' , 'DRAFT-only' )
// Advance through an actual edit, so stale version 0 remains valid input rather than Min(0) validation failure.
const editCommand = freezeOrderCommand ( original , key ( context , `platform- ${ action } -prepare-edit` ), { draft : draftPayload })
context . apiSuccess ( MODULE , `Platform ${ action } 形成真实旧页面版本` ,
await context . request ( 'PUT' , path , editCommand . body , context . token , editCommand . headers ))
const current = await read ( '冻结动作首次有效请求版本' )
assertFact ( '编辑后仍为草稿且版本增加' , null , current . order . status === 'DRAFT' && current . order . version > original . order . version , 'old-page-version-created' )
const extra = action === 'cancel' ? { reason : ` ${ context . marker } independent draft cancellation` } : {}
const command = freezeOrderCommand ( current , key ( context , `platform- ${ action } -execute` ), extra )
proof . originalVersion = original . order . version
proof . command = command
const send = async ( label , body , headers ) => {
proof . commands . push ({ label , path : mutationPath , body : structuredClone ( body ), headers : structuredClone ( headers ) })
return context . request ( 'POST' , mutationPath , body , context . token , headers )
}
const reject = async ( label , body , headers , code , expected , statuses = [ 200 ]) => {
const response = await send ( label , body , headers )
assertFact ( label , response , statuses . includes ( response ? . http ) && String ( response . body ? . code ) === code , `expected= ${ code } ` )
same ( ` ${ label } 后零漂移` , expected , await read ( ` ${ label } 后回查` ))
proof . checks . push ({ label , code })
}
await reject ( '首次旧页面版本拒绝' , { ... extra , expectedVersion : original . order . version },
{ 'Idempotency-Key' : key ( context , `platform- ${ action } -stale-first` ) }, 'A0409' , current )
await reject ( '首次缺版本拒绝' , extra ,
{ 'Idempotency-Key' : key ( context , `platform- ${ action } -missing-version` ) }, 'A0422' , current )
await reject ( '首次缺幂等键拒绝' , command . body , {}, 'A0400' , current , [ 200 , 400 ])
context . apiSuccess ( MODULE , `Platform ${ action } 原版本原键成功` , await send ( 'success' , command . body , command . headers ))
const completed = await read ( '成功后的事实快照' )
const expectedStatus = action === 'submit' ? 'SUBMITTED' : 'CANCELLED'
const oldHistoryIds = new Set ( current . statusHistory . map ( row => id ( row . id )))
const added = completed . statusHistory . filter ( row => ! oldHistoryIds . has ( id ( row . id )))
assertFact ( '成功状态版本与唯一动作历史' , null , completed . order . status === expectedStatus && completed . order . version > current . order . version
&& added . length === 1 && added [ 0 ]. action === expectedStatus && added [ 0 ]. fromStatus === 'DRAFT' && added [ 0 ]. toStatus === expectedStatus
&& Boolean ( added [ 0 ]. operatorId && added [ 0 ]. occurredAt )
&& ( action !== 'cancel' || added [ 0 ]. reason === extra . reason ), 'one attributed lifecycle history' )
assertFact ( '成功后货物旧历史及业务身份保留' , null ,
[ 'cargoes' , 'parties' , 'internalNotes' ]. every ( field => JSON . stringify ( current [ field ]) === JSON . stringify ( completed [ field ]))
&& JSON . stringify ( current . statusHistory ) === JSON . stringify ( completed . statusHistory . filter ( row => oldHistoryIds . has ( id ( row . id ))))
&& [ 'id' , 'tenantId' , 'customerId' , 'orderNo' ]. every ( field => current . order [ field ] === completed . order [ field ]), 'no unrelated mutations' )
context . apiSuccess ( MODULE , `Platform ${ action } 原键原版本精确重放成功` , await send ( 'replay' , command . body , command . headers ))
same ( '成功重放后零漂移' , completed , await read ( '原键重放后回查' ))
const changed = action === 'submit' ? { expectedVersion : completed . order . version } : { ... command . body , reason : ` ${ extra . reason } -changed` }
await reject ( '原键异载荷拒绝' , changed , command . headers , 'A0409' , completed )
await reject ( '新键重复动作拒绝' , { ... extra , expectedVersion : completed . order . version },
{ 'Idempotency-Key' : key ( context , `platform- ${ action } -repeat-new-key` ) }, 'A0409' , completed )
proof . finalStatus = expectedStatus
proof . finalFactsSha256 = createHash ( 'sha256' ). update ( orderPersistenceFacts ( completed )). digest ( 'hex' )
}
return proofs
}
2026-09-15 12:07:43 +08:00
export function fixtureInboundCode ( policy , marker ) {
if ( policy ? . mode === 'AUTO' && policy . manualEntryRequired === false ) return {}
if ( policy ? . mode !== 'LEGACY_MANUAL' || policy . manualEntryRequired !== true || ! marker ) {
throw new Error ( '入仓码策略缺失或不一致,停止创建测试订单' )
}
// Short enough for the 34-character carton QR limit; stable within this fresh fixture.
return { inboundCode : `AT ${ createHash ( 'sha256' ). update ( marker ). digest ( 'hex' ). slice ( 0 , 20 ). toUpperCase () } ` }
}
2026-09-30 17:13:11 +08:00
export function cargoRequirementNegativeCases ( payload ) {
const cases = [
{ name : '木制品未选择商检' , at : 'CREATE' , mutate : value => { value . customsInspection = false ; value . cargoes [ 0 ]. woodenProduct = true } },
{ name : '货物零箱数' , at : 'CREATE' , mutate : value => { value . cargoes [ 0 ]. packageCount = 0 } },
{ name : '货物总件数与每箱件数不守恒' , at : 'CREATE' , mutate : value => { value . cargoes [ 0 ]. totalQuantity ++ } },
]
if ( payload . destinationType === 'FBA' ) {
cases . push ({ name : 'FBA缺少订单货件编号' , at : 'SUBMIT' , mutate : value => { value . shipmentId = null } },
{ name : 'FBA缺少逐行跟踪编号' , at : 'SUBMIT' , mutate : value => { value . cargoes [ 0 ]. referenceId = null } })
}
if ( payload . fulfillmentType === 'ONE_CLICK_DROPSHIP' ) {
cases . push ({ name : '一件代发缺少SKU' , at : 'CREATE_OR_SUBMIT' , mutate : value => { value . cargoes [ 0 ]. skuNo = null } },
{ name : '一件代发混箱组箱规冲突' , at : 'SUBMIT' , mutate : value => {
const extra = structuredClone ( value . cargoes [ 0 ]); extra . skuNo = ` ${ extra . skuNo } -B` . slice ( 0 , 64 ); extra . lengthCm ++
value . cargoes . push ( extra )
} })
}
return cases . map (({ mutate , ... item }) => { const body = structuredClone ( payload ); mutate ( body ); return { ... item , body } })
}
async function verifyCargoRequirementNegatives ( context , payload , policy , merchantToken ) {
const base = '/api/merchant/v1/orders/first-leg'
const cases = cargoRequirementNegativeCases ( payload )
const evidence = []
for ( const [ index , item ] of cases . entries ()) {
const body = { ... item . body , ... fixtureInboundCode ( policy , ` ${ context . marker } -NEG- ${ index } ` ) }
const created = await request ( context , 'POST' , base , body , merchantToken ,
{ 'Idempotency-Key' : key ( context , `cargo-negative- ${ index } -create` ) })
if ( item . at === 'CREATE' || String ( created ? . body ? . code ) !== '200' ) {
context . assert ( MODULE , ` ${ payload . destinationType } / ${ payload . fulfillmentType } ${ item . name } 创建拒绝` , created ,
created . http === 200 && String ( created . body ? . code ) === 'A0422' ,
`HTTP= ${ created . http } , code= ${ created . body ? . code || 'missing' } ` , CONTRACT_OPTIONS )
evidence . push ({ name : item . name , stage : 'CREATE' , code : created . body ? . code ,
orderId : String ( created . body ? . code ) === '200' ? id ( created . body ? . data ) : null })
continue
}
const orderId = id ( created . body ? . data )
if ( ! orderId ) throw new Error ( 'negative draft missing order id' )
const before = await request ( context , 'GET' , ` ${ base } / ${ orderId } ` , undefined , merchantToken )
2026-10-02 23:14:24 +08:00
const submitCommand = freezeOrderCommand ( context . apiSuccess ( MODULE , '负向提交冻结草稿版本' , before ), key ( context , `cargo-negative- ${ index } -submit` ))
const submitted = await request ( context , 'POST' , ` ${ base } / ${ orderId } /submit` , submitCommand . body , merchantToken , submitCommand . headers )
2026-09-30 17:13:11 +08:00
const after = await request ( context , 'GET' , ` ${ base } / ${ orderId } ` , undefined , merchantToken )
context . assert ( MODULE , ` ${ payload . destinationType } / ${ payload . fulfillmentType } ${ item . name } 提交拒绝且草稿零漂移` , submitted ,
submitted . http === 200 && String ( submitted . body ? . code ) === 'A0422'
&& String ( before . body ? . code ) === '200' && String ( after . body ? . code ) === '200'
&& after . body . data ? . order ? . status === 'DRAFT'
&& JSON . stringify ( before . body . data ) === JSON . stringify ( after . body . data ),
`orderId= ${ orderId } , code= ${ submitted . body ? . code || 'missing' } ` , CONTRACT_OPTIONS )
evidence . push ({ name : item . name , stage : 'SUBMIT' , code : submitted . body ? . code , orderId })
}
context . createdResources . orderCargoNegatives ??= []
context . createdResources . orderCargoNegatives . push ({ marker : context . marker ,
destinationType : payload . destinationType , fulfillmentType : payload . fulfillmentType , cases : evidence })
}
2026-09-02 12:19:46 +08:00
function declarationCargo ( source , index ) {
const number = ( value ) => Number . isFinite ( Number ( value )) ? Number ( value ) : 0
const optionalNumber = ( value ) => value === null || value === undefined || value === '' ? null : number ( value )
return {
itemNo : number ( source ? . itemNo ) || index + 1 ,
cargoName : String ( source ? . cargoName || '' ), cargoNameEn : source ? . cargoNameEn || null ,
skuNo : source ? . skuNo || null , model : source ? . model || null ,
packageCount : number ( source ? . packageCount ), totalQuantity : number ( source ? . totalQuantity ),
lengthCm : number ( source ? . lengthCm ), widthCm : number ( source ? . widthCm ), heightCm : number ( source ? . heightCm ),
grossWeightKg : number ( source ? . grossWeightKg ), volumeCbm : number ( source ? . volumeCbm ),
netWeightKg : optionalNumber ( source ? . netWeightKg ), hsCode : source ? . hsCode || null ,
customsHsCode : source ? . customsHsCode || null , unitsPerPackage : optionalNumber ( source ? . unitsPerPackage ),
unitDeclaredValue : optionalNumber ( source ? . unitDeclaredValue ), declaredValue : optionalNumber ( source ? . declaredValue ),
currency : source ? . currency || null , material : source ? . material || null , usage : source ? . usage || null ,
brand : source ? . brand || null , cargoRemark : source ? . cargoRemark || source ? . remark || null ,
}
}
2026-09-01 12:02:35 +08:00
export function orderPersistenceFacts ( detail ) {
return JSON . stringify ({
order : detail ? . order ?? null ,
cargoes : Array . isArray ( detail ? . cargoes ) ? detail . cargoes : [],
parties : Array . isArray ( detail ? . parties ) ? detail . parties : [],
internalNotes : Array . isArray ( detail ? . internalNotes ) ? detail . internalNotes : [],
statusHistory : Array . isArray ( detail ? . statusHistory ) ? detail . statusHistory : [],
})
}
2026-10-03 05:25:30 +08:00
// Read-only planning and evidence validation only. These exports never configure services or send requests.
export function buildOrderDependencyFaultPlan ( fixture ) {
const customerId = id ( fixture ? . customerId )
if ( ! /^[1-9]\d*$/ . test ( customerId ) || customerId !== id ( fixture ? . registeredCustomerId )
|| ! /^[A-Za-z0-9_-]{1,80}$/ . test ( fixture ? . marker || '' )) throw new Error ( 'Dependency fault fixture must belong to this run registered customer' )
const commands = {}
for ( const role of [ 'merchant' , 'platform' ]) {
const detail = fixture [ role ]
if ( id ( detail ? . order ? . tenantId ) !== customerId || ! /^[1-9]\d*$/ . test ( id ( detail ? . order ? . id ))
|| detail . order . status !== 'DRAFT' || ! detail . order . orderNo
|| ! [ 'cargoes' , 'parties' , 'internalNotes' , 'statusHistory' ]. every ( field => Array . isArray ( detail [ field ]))) {
throw new Error ( `Incomplete owned ${ role } DRAFT evidence` )
}
const command = freezeOrderCommand ( detail , `dep- ${ role } - ${ fixture . marker } ` )
commands [ role ] = { method : 'POST' , path : `/api/ ${ role } /v1/orders/first-leg/ ${ detail . order . id } /submit ${ role === 'platform' ? `?tenantId= ${ customerId } ` : '' } ` ,
... command , orderId : id ( detail . order . id ), orderNo : detail . order . orderNo ,
initialOrderSha256 : createHash ( 'sha256' ). update ( orderPersistenceFacts ( detail )). digest ( 'hex' ) }
}
if ( commands . merchant . orderId === commands . platform . orderId ) throw new Error ( 'Use two independent drafts for same-key recovery' )
return { marker : fixture . marker , customerId , runtimeChangesExecuted : false , commands ,
phases : [
{ dependency : 'IAM' , urlProperty : 'baiyue.clients.iam.url' , targetMethod : 'GET' , targetPath : `/internal/iam/tenants/ ${ customerId } /order-eligibility` ,
faultHttp : 503 , expectedHttp : 200 , expectedCode : 'S0503' , expectedMessages : [ 'IAM eligibility service is unavailable' , 'IAM 下单资格响应无效' ] },
{ dependency : 'FINANCE' , urlProperty : 'baiyue.clients.finance.url' , targetMethod : 'GET' , targetPath : `/internal/finance/v1/wallet/accounts/ ${ customerId } ` ,
faultHttp : 503 , expectedHttp : 200 , expectedCode : 'S0503' , expectedMessages : [ 'Finance account service is unavailable' , 'Finance account response is invalid' ] },
],
recovery : 'Restore original Order image/config/network identity; submit each original frozen command once, then replay it; require one submission history and no duplicate Firstleg facts.' }
}
export function assertOrderDependencyFaultObservation ( plan , dependency , role , observation ) {
const phase = plan ? . phases ? . find ( item => item . dependency === dependency ), command = plan ? . commands ? .[ role ]
const require = ( ok , message ) => { if ( ! ok ) throw new Error ( `Dependency fault evidence: ${ message } ` ) }
require ( Boolean ( phase && command ), 'unknown dependency or role' )
const request = observation ? . request , response = observation ? . response
require ( request ? . method === command . method && request . path === command . path
&& JSON . stringify ( request . body ) === JSON . stringify ( command . body )
&& request . headers ? .[ 'Idempotency-Key' ] === command . headers [ 'Idempotency-Key' ], 'original frozen request/version/key changed' )
const requestId = request . headers ? .[ 'X-Request-Id' ]
require ( typeof requestId === 'string' && requestId . length > 0 , 'missing correlation ID' )
require ( response ? . http === phase . expectedHttp && String ( response . body ? . code ) === phase . expectedCode
&& phase . expectedMessages . includes ( response . body . message ), 'must be the real Order dependency failure, not auth/version/business validation' )
require ( Array . isArray ( observation . proxyRecords ) && observation . proxyRecords . some ( row =>
row . requestId === requestId && row . method === phase . targetMethod && row . path === phase . targetPath
&& row . matched === true && row . responseStatus === phase . faultHttp && row . forwarded === false
&& row . internalTokenPresent === true ), 'missing exact authenticated upstream fault hit' )
require ( observation . proxyRecords . filter ( row => row . matched === true ). every ( row => row . method === phase . targetMethod
&& row . path === phase . targetPath && row . responseStatus === phase . faultHttp && row . forwarded === false ), 'fault escaped selected customer/path' )
for ( const label of [ 'before' , 'after' ]) {
const snapshot = observation [ label ], detail = snapshot ? . detail
require ( snapshot ? . inboundPageComplete === true && Array . isArray ( snapshot . inbounds )
&& [ 'cargoes' , 'parties' , 'internalNotes' , 'statusHistory' ]. every ( field => Array . isArray ( detail ? .[ field ]))
&& id ( detail ? . order ? . id ) === command . orderId && id ( detail . order . tenantId ) === plan . customerId
&& detail . order . status === 'DRAFT' , ` ${ label } incomplete/foreign order or Firstleg evidence` )
require ( snapshot . task === null || snapshot . task && id ( snapshot . task . orderId ) === command . orderId
&& id ( snapshot . task . tenantId ) === plan . customerId , ` ${ label } task source mismatch` )
require ( snapshot . inbounds . every ( row => id ( row . tenantId ) === plan . customerId && row . orderNo === command . orderNo ), ` ${ label } inbound source mismatch` )
}
require ( createHash ( 'sha256' ). update ( orderPersistenceFacts ( observation . before . detail )). digest ( 'hex' ) === command . initialOrderSha256 ,
'baseline differs from prepared draft' )
require ( JSON . stringify ( observation . before ) === JSON . stringify ( observation . after ), 'order/version/history/Firstleg facts changed' )
return { dependency , role , orderId : command . orderId , verified : true , requestId ,
snapshotSha256 : createHash ( 'sha256' ). update ( JSON . stringify ( observation . after )). digest ( 'hex' ) }
}
2026-08-24 19:58:35 +08:00
export function requestedOrderDestinationType ( context ) {
2026-08-26 11:10:42 +08:00
const value = String ( context ? . context ? . orderDestinationType || 'FBA' ). trim (). toUpperCase ()
return [ 'FBA' , 'OVERSEAS' , 'PRIVATE' ]. includes ( value ) ? value : 'FBA'
2026-08-24 19:58:35 +08:00
}
2026-09-10 21:53:15 +08:00
export function eligibleOrderDestinations ( values , destinationType ) {
const active = ( Array . isArray ( values ) ? values : []). filter (( warehouse ) => warehouse ? . status === 'ACTIVE' )
return destinationType === 'FBA'
? active . filter (( warehouse ) => String ( warehouse ? . internalCode || '' ). trim () !== '' )
: active
}
2026-08-13 18:34:44 +08:00
export function approvedInboundReady ( orderDetail , taskResponse , tenantId , orderId ) {
const summary = orderDetail ? . summary
const task = success ( taskResponse ) ? taskResponse . body ? . data : null
return Boolean ( summary ? . inboundId )
&& MEASURABLE_STATUSES . has ( summary ? . mainStatus )
&& id ( task ? . orderId ) === id ( orderId )
&& id ( task ? . tenantId ) === id ( tenantId )
}
2026-09-30 15:16:39 +08:00
export function approvalReplayDigest ( snapshot ) {
return createHash ( 'sha256' ). update ( JSON . stringify ( snapshot )). digest ( 'hex' )
}
export async function verifyOrderApprovalReplay ( context , { tenantId , orderId , inboundId , inboundCode }) {
const scope = `tenantId= ${ encodeURIComponent ( tenantId ) } `
const orderPath = `/api/platform/v1/orders/first-leg/ ${ encodeURIComponent ( orderId ) } ? ${ scope } `
const taskPath = `/api/platform/v1/first-leg/tasks/by-order/ ${ encodeURIComponent ( orderId ) } ? ${ scope } `
const inboundPath = `/api/platform/inbounds/ ${ encodeURIComponent ( inboundId ) } ? ${ scope } `
async function read ( name ) {
const orderResponse = await context . request ( 'GET' , orderPath )
const detail = context . apiSuccess ( MODULE , ` ${ name } 订单及状态历史` , orderResponse , '' , CONTRACT_OPTIONS )
const taskResponse = await context . request ( 'GET' , taskPath )
const task = context . apiSuccess ( MODULE , ` ${ name } 原履约任务` , taskResponse , '' , CONTRACT_OPTIONS )
const inboundResponse = await context . request ( 'GET' , inboundPath )
const inbound = context . apiSuccess ( MODULE , ` ${ name } 原入库单及历史` , inboundResponse , '' , CONTRACT_OPTIONS )
const rows = []; let total = null ; let page = 1
while ( total == null || rows . length < total ) {
const response = await context . request ( 'GET' , `/api/platform/inbounds? ${ scope } `
+ `&inboundCode= ${ encodeURIComponent ( inboundCode ) } &page= ${ page } &size=100` )
const data = context . apiSuccess ( MODULE , ` ${ name } 入仓码来源入库列表第 ${ page } 页` , response , '' , CONTRACT_OPTIONS )
const valid = pageOk ( data ) && Number . isInteger ( Number ( data . total )) && Number ( data . total ) >= 0
&& ( total == null || total === Number ( data . total ))
&& ( data . records . length > 0 || Number ( data . total ) === 0 )
context . assert ( MODULE , ` ${ name } 入库分页计数完整` , response , valid ,
`page= ${ page } , total= ${ data ? . total ?? 'missing' } ` , CONTRACT_OPTIONS )
if ( ! valid ) throw new Error ( '审核重放核验缺少完整入库分页事实' )
total = Number ( data . total ); rows . push (... data . records ); page += 1
}
const exact = rows . filter ( row => id ( row . inboundCode ) === inboundCode )
const valid = detail ? . order ? . status === 'APPROVED' && id ( detail . order . id ) === orderId
&& id ( detail . order . inboundCode ) === inboundCode && Array . isArray ( detail . statusHistory )
&& id ( task ? . orderId ) === orderId && id ( task ? . tenantId ) === tenantId && Boolean ( id ( task ? . id ))
&& id ( inbound ? . summary ? . id ) === inboundId && id ( inbound ? . summary ? . tenantId ) === tenantId
&& id ( inbound ? . summary ? . inboundCode ) === inboundCode && Array . isArray ( inbound ? . statusHistory )
&& rows . length === total && new Set ( rows . map ( row => id ( row . id ))). size === total
&& exact . length === 1 && id ( exact [ 0 ]. id ) === inboundId
context . assert ( MODULE , ` ${ name } 审核态原身份与唯一入库事实有效` , inboundResponse , valid ,
`exactInbounds= ${ exact . length } , taskId= ${ id ( task ? . id ) } , inboundId= ${ id ( inbound ? . summary ? . id ) } ` , CONTRACT_OPTIONS )
if ( ! valid ) throw new Error ( '审核重放核验的订单任务或唯一入库身份不一致' )
return { response : inboundResponse , facts : {
order : JSON . parse ( orderPersistenceFacts ( detail )), task , inbound , exactInbounds : exact ,
} }
}
const before = await read ( '审核重放前' )
const response = await context . request ( 'POST' ,
`/api/platform/v1/orders/first-leg/ ${ encodeURIComponent ( orderId ) } /approve? ${ scope } ` )
const rejected = response ? . http === 200 && String ( response . body ? . code ) === 'A0409'
context . assert ( MODULE , '已审核订单再次审核明确A0409' , response , rejected ,
`HTTP= ${ response ? . http } , code= ${ response ? . body ? . code ?? 'missing' } ` , CONTRACT_OPTIONS )
const after = await read ( '审核重放后' )
const beforeDigest = approvalReplayDigest ( before . facts )
const afterDigest = approvalReplayDigest ( after . facts )
const unchanged = beforeDigest === afterDigest
context . assert ( MODULE , '审核重复拒绝后订单入仓码历史任务及唯一入库事实零漂移' , after . response ,
unchanged , `inboundCount=1, orderHistories= ${ after . facts . order . statusHistory . length } `
+ `, inboundHistories= ${ after . facts . inbound . statusHistory . length } ` , CONTRACT_OPTIONS )
const evidence = { marker : context . marker , tenantId , orderId , inboundId , inboundCode ,
taskId : id ( after . facts . task . id ), inboundCount : after . facts . exactInbounds . length ,
orderHistoryCount : after . facts . order . statusHistory . length ,
inboundHistoryCount : after . facts . inbound . statusHistory . length ,
beforeDigest , afterDigest , rejected , unchanged }
context . createdResources . orders . approvalReplay = evidence
return evidence
}
2026-09-30 17:13:11 +08:00
export async function finishOrderLifecycle ( context , { tenantId , orderId , firstLegDetailPath }) {
const stopAtApproved = context . context ? . orderStopAtApproved === true
if ( ! stopAtApproved ) {
const acceptResponse = await context . request ( 'POST' ,
`/api/platform/v1/orders/first-leg/ ${ encodeURIComponent ( orderId ) } /accept?tenantId= ${ encodeURIComponent ( tenantId ) } ` )
context . apiSuccess ( MODULE , 'Admin 受理本轮订单' , acceptResponse )
const startResponse = await context . request ( 'POST' ,
`/api/platform/v1/orders/first-leg/ ${ encodeURIComponent ( orderId ) } /start?tenantId= ${ encodeURIComponent ( tenantId ) } ` )
context . apiSuccess ( MODULE , 'Admin 启动本轮订单履约' , startResponse )
}
const finalDetailResponse = await context . request ( 'GET' , firstLegDetailPath )
const finalDetail = context . apiSuccess ( MODULE , 'Admin 回查本轮订单履约状态' , finalDetailResponse )
const history = finalDetail ? . statusHistory
const valid = finalDetail ? . order ? . status === ( stopAtApproved ? 'APPROVED' : 'PROCESSING' )
&& history ? . some ? .(( item ) => item . toStatus === 'APPROVED' )
&& ( stopAtApproved
? ! history . some (( item ) => [ 'ACCEPTED' , 'PROCESSING' ]. includes ( item . toStatus ))
: history . some (( item ) => item . toStatus === 'ACCEPTED' )
&& history . some (( item ) => item . toStatus === 'PROCESSING' ))
context . assert ( MODULE , stopAtApproved ? '显式审批停点保留原始APPROVED且未受理启动' : '本轮订单已进入处理中且保留状态历史' ,
finalDetailResponse , valid ,
`status= ${ finalDetail ? . order ? . status ?? 'missing' } , history= ${ history ? . length ?? 'n/a' } ` , CONTRACT_OPTIONS )
if ( stopAtApproved && ! valid ) throw new Error ( '显式审批停点未保留原始APPROVED,不能继续准备该专项' )
context . createdResources . orders . rawOrderStatus = finalDetail ? . order ? . status || null
context . createdResources . orders . stoppedAtApproved = stopAtApproved
}
2026-08-08 10:11:04 +08:00
function tomorrow ( days = 30 ) {
const date = new Date (); date . setUTCDate ( date . getUTCDate () + days )
return date . toISOString (). slice ( 0 , 10 )
}
2026-08-24 19:58:35 +08:00
function pricingEstimateRequest ( receiving , channel , pricingTaxMode , destination , destinationType = 'FBA' ) {
2026-08-13 19:00:33 +08:00
return {
originWarehouseId : receiving . id ,
channelCode : channel . channelCode ,
destinationCode : destination . warehouseCode ,
2026-08-24 19:58:35 +08:00
destinationType ,
2026-08-13 19:00:33 +08:00
destinationWarehouseId : destination . id ,
destinationName : destination . warehouseName ,
pricingTaxMode ,
actualWeightKg : ORDER_ESTIMATE_WEIGHT_KG ,
actualVolumeCbm : ORDER_ESTIMATE_VOLUME_CBM ,
}
}
function estimateMatches ( response , candidate ) {
const estimate = success ( response ) ? response . body ? . data : null
return Boolean ( estimate )
&& id ( estimate . priceBookVersionId ) === id ( candidate . channel . priceBookVersionId )
&& id ( estimate . channelCode ) === id ( candidate . channel . channelCode )
&& id ( estimate . pricingTaxMode ) === id ( candidate . pricingTaxMode )
}
export async function findPricedOrderRoute ({ receiving , destinations , channels , estimate ,
2026-08-24 19:58:35 +08:00
concurrency = PRICING_PROBE_CONCURRENCY , destinationType = 'FBA' }) {
2026-08-13 19:00:33 +08:00
const candidates = []
for ( const channel of channels ) {
if ( ! channel ? . priceBookVersionId || ! channel ? . channelCode || ! Array . isArray ( channel . pricingTaxModes )) continue
for ( const pricingTaxMode of channel . pricingTaxModes ) {
if ( ! pricingTaxMode ) continue
for ( const destination of destinations ) {
if ( ! destination ? . id || ! destination ? . warehouseCode ) continue
candidates . push ({ channel , pricingTaxMode , destination ,
2026-08-24 19:58:35 +08:00
request : pricingEstimateRequest ( receiving , channel , pricingTaxMode , destination , destinationType ) })
2026-08-13 19:00:33 +08:00
}
}
}
const failures = []
let lastResponse = null
const batchSize = Math . max ( 1 , Math . min ( Number ( concurrency ) || 1 , PRICING_PROBE_CONCURRENCY ))
let attempts = 0
for ( let offset = 0 ; offset < candidates . length ; offset += batchSize ) {
const batch = candidates . slice ( offset , offset + batchSize )
const results = await Promise . all ( batch . map ( async ( candidate ) => ({
candidate , response : await estimate ( candidate . request ),
})))
attempts += results . length
lastResponse = results . at ( - 1 ) ? . response || lastResponse
const match = results . find (({ candidate , response }) => estimateMatches ( response , candidate ))
if ( match ) return { priced : match . candidate , response : match . response , attempts , failures }
for ( const { candidate , response } of results ) {
const returned = success ( response ) ? response . body ? . data : null
failures . push ({
tuple : ` ${ candidate . channel . channelCode } / ${ candidate . pricingTaxMode } / ${ candidate . destination . warehouseCode } ` ,
http : response ? . http ?? 'missing' , code : response ? . body ? . code ?? 'missing' ,
requestId : response ? . requestId ?? 'missing' , traceId : response ? . traceId ?? 'missing' ,
mismatch : returned
? `version= ${ id ( returned . priceBookVersionId ) || 'missing' } ,channel= ${ id ( returned . channelCode ) || 'missing' } ,tax= ${ id ( returned . pricingTaxMode ) || 'missing' } `
: null ,
})
}
}
return { priced : null , response : lastResponse , attempts , failures }
}
2026-08-15 11:36:53 +08:00
export async function findPricedOrderRouteAcrossOrigins ({ receivings , destinations , loadChannels , estimate ,
2026-08-24 19:58:35 +08:00
concurrency = PRICING_PROBE_CONCURRENCY , destinationType = 'FBA' }) {
2026-08-15 11:36:53 +08:00
const originFailures = []
let attempts = 0
let response = null
for ( const receiving of receivings ) {
if ( ! receiving ? . id ) continue
const channelResponse = await loadChannels ( receiving )
response = channelResponse || response
const channels = success ( channelResponse ) && Array . isArray ( channelResponse . body ? . data )
? channelResponse . body . data : []
const priceableChannels = channels . filter (( item ) => Array . isArray ( item . pricingTaxModes ) && item . pricingTaxModes . length > 0 )
if ( priceableChannels . length === 0 ) {
originFailures . push ({ originWarehouseId : id ( receiving . id ), stage : 'channels' , http : channelResponse ? . http ?? 'missing' ,
code : channelResponse ? . body ? . code ?? 'missing' , requestId : channelResponse ? . requestId ?? 'missing' ,
traceId : channelResponse ? . traceId ?? 'missing' , count : channels . length })
continue
}
2026-09-30 17:13:11 +08:00
if ( destinationType === 'PRIVATE' ) {
// Private destinations use customer-owned addresses, not warehouse-rate
// destination rows. Match the same authoritative channel path already
// used by fixed-origin private orders.
const channel = priceableChannels [ 0 ]
return { receiving , priced : { channel , pricingTaxMode : channel . pricingTaxModes [ 0 ], destination : null },
response : channelResponse , attempts : 0 , failures : [], originFailures }
}
2026-08-24 19:58:35 +08:00
const result = await findPricedOrderRoute ({ receiving , destinations , channels : priceableChannels ,
estimate , concurrency , destinationType })
2026-08-15 11:36:53 +08:00
attempts += result . attempts
response = result . response || response
if ( result . priced ) return { receiving , ... result , originFailures }
originFailures . push ({ originWarehouseId : id ( receiving . id ), stage : 'estimate' , attempts : result . attempts ,
evidence : pricingFailureEvidence ( result ) })
}
return { receiving : null , priced : null , response , attempts , originFailures }
}
2026-08-13 19:00:33 +08:00
function pricingFailureEvidence ( result ) {
const describe = ( failure ) => failure
? ` ${ failure . tuple } [HTTP= ${ failure . http } ,code= ${ failure . code } ,requestId= ${ failure . requestId } ,traceId= ${ failure . traceId }${ failure . mismatch ? `,response= ${ failure . mismatch } ` : '' } ]`
: 'missing'
return `没有可计价组合;attempts= ${ result . attempts } , first= ${ describe ( result . failures [ 0 ]) } , last= ${ describe ( result . failures . at ( - 1 )) } `
}
2026-08-15 11:36:53 +08:00
function originPricingFailureEvidence ( result ) {
const describe = ( failure ) => ! failure ? 'missing'
: failure . stage === 'channels'
? `origin= ${ failure . originWarehouseId } ,channels[HTTP= ${ failure . http } ,code= ${ failure . code } ,count= ${ failure . count } ,requestId= ${ failure . requestId } ,traceId= ${ failure . traceId } ]`
: `origin= ${ failure . originWarehouseId } ,estimate[attempts= ${ failure . attempts } , ${ failure . evidence } ]`
return `没有 ACTIVE 国内仓可计价组合;origins= ${ result . originFailures ? . length ?? 0 } , attempts= ${ result . attempts } , first= ${ describe ( result . originFailures ? .[ 0 ]) } , last= ${ describe ( result . originFailures ? . at ( - 1 )) } `
}
2026-08-08 10:11:04 +08:00
async function request ( context , method , path , body , token , extraHeaders = {}) {
const headers = { Accept : 'application/json' , Authorization : `Bearer ${ token } ` , ... extraHeaders }
let payload = body
if ( body !== undefined && ! ( body instanceof FormData )) {
headers [ 'Content-Type' ] = 'application/json; charset=utf-8'
payload = JSON . stringify ( body )
}
try {
const response = await fetch ( ` ${ context . options . gateway }${ path } ` , {
method , headers , body : payload , signal : AbortSignal . timeout ( 30_000 ),
})
const content = await response . text ()
let parsed = null
try { parsed = content ? JSON . parse ( content ) : null } catch { /* Recorded as a bounded raw response. */ }
return {
method , path , http : response . status , body : parsed ,
raw : parsed === null ? content . slice ( 0 , 500 ) : null ,
requestId : parsed ? . requestId || response . headers . get ( 'x-request-id' ),
traceId : parsed ? . traceId || response . headers . get ( 'x-trace-id' ),
}
} catch ( error ) {
return { method , path , http : 0 , body : null , raw : String ( error ), requestId : null , traceId : null }
}
}
async function uploadProductImage ( context , merchantToken ) {
const form = new FormData ()
const png = Buffer . from ( 'iVBORw0KGgoAAAANSUhEUgAAAAEAAAABCAQAAAC1HAwCAAAAC0lEQVR42mNk+A8AAQUBAScY42YAAAAASUVORK5CYII=' , 'base64' )
form . append ( 'file' , new Blob ([ png ], { type : 'image/png' }), `product- ${ context . marker } .png` )
const response = await request ( context , 'POST' , '/api/merchant/v1/files/product-images' , form , merchantToken )
const data = context . apiSuccess ( MODULE , 'Client 上传产品图片' , response , 'contentType=image/png' , CONTRACT_OPTIONS )
context . assert ( MODULE , '产品图片上传返回字符串文件 ID' , response ,
Boolean ( data ? . fileId ) && typeof data . fileId === 'string' && data . contentType === 'image/png' ,
`fileIdType= ${ typeof data ? . fileId } , contentType= ${ data ? . contentType ?? 'missing' } ` , CONTRACT_OPTIONS )
return id ( data ? . fileId )
}
async function assertProductImageDownload ( context , merchantToken , fileId ) {
const path = `/api/merchant/v1/files/product-images/ ${ encodeURIComponent ( fileId ) } `
try {
const response = await fetch ( ` ${ context . options . gateway }${ path } ` , {
headers : { Accept : 'image/*' , Authorization : `Bearer ${ merchantToken } ` },
signal : AbortSignal . timeout ( 30_000 ),
})
const bytes = await response . arrayBuffer ()
const evidence = { method : 'GET' , path , http : response . status , body : null , raw : null ,
requestId : response . headers . get ( 'x-request-id' ), traceId : response . headers . get ( 'x-trace-id' ) }
context . assert ( MODULE , 'Client 可下载本轮产品图片' , evidence ,
response . status === 200 && bytes . byteLength > 0 && ( response . headers . get ( 'content-type' ) || '' ). startsWith ( 'image/' ),
`bytes= ${ bytes . byteLength } , contentType= ${ response . headers . get ( 'content-type' ) || 'missing' } ` , CONTRACT_OPTIONS )
} catch ( error ) {
context . assert ( MODULE , 'Client 可下载本轮产品图片' , null , false , String ( error ), CONTRACT_OPTIONS )
}
}
async function loadMasterData ( context , merchantToken ) {
2026-08-24 19:58:35 +08:00
const destinationType = requestedOrderDestinationType ( context )
2026-08-26 11:10:42 +08:00
const fixedRoute = context . context ? . waitSplitRoute || {}
2026-09-23 22:00:00 +08:00
const domesticResponse = await requestTransportRetry ( context , 'GET' ,
2026-08-08 10:11:04 +08:00
'/api/merchant/v1/warehouses?page=1&size=200&status=ACTIVE&warehouseType=DOMESTIC' , undefined , merchantToken )
const domesticPage = context . apiSuccess ( MODULE , 'Client 查询国内收货仓' , domesticResponse )
context . assert ( MODULE , '存在可用国内收货仓' , domesticResponse , records ( domesticPage ). length > 0 ,
`count= ${ records ( domesticPage ). length } ` )
2026-08-26 11:10:42 +08:00
let destinationResponse = null
let destinationPage = null
if ( destinationType !== 'PRIVATE' ) {
2026-09-23 22:00:00 +08:00
destinationResponse = await requestTransportRetry ( context , 'GET' ,
2026-08-26 11:10:42 +08:00
`/api/merchant/v1/warehouses?page=1&size=200&status=ACTIVE&warehouseType= ${ encodeURIComponent ( destinationType ) } ` ,
undefined , merchantToken )
destinationPage = context . apiSuccess ( MODULE , `Client 查询 ${ destinationType } 目的仓` , destinationResponse )
context . assert ( MODULE , `存在可用 ${ destinationType } 目的仓` , destinationResponse ,
records ( destinationPage ). length > 0 , `count= ${ records ( destinationPage ). length } ` )
}
2026-08-15 11:36:53 +08:00
const receivings = records ( domesticPage ). filter (( warehouse ) => warehouse ? . status === 'ACTIVE' )
2026-09-10 21:53:15 +08:00
let destinations = eligibleOrderDestinations ( records ( destinationPage ), destinationType )
2026-08-26 11:10:42 +08:00
if ( Number ( destinationPage ? . total ) > destinations . length ) {
2026-09-23 22:00:00 +08:00
const nextResponse = await requestTransportRetry ( context , 'GET' ,
2026-08-26 11:10:42 +08:00
`/api/merchant/v1/warehouses?page=2&size=200&status=ACTIVE&warehouseType= ${ encodeURIComponent ( destinationType ) } ` ,
undefined , merchantToken )
const nextPage = context . apiSuccess ( MODULE , `Client 查询 ${ destinationType } 目的仓第二页` , nextResponse )
2026-09-10 21:53:15 +08:00
destinations = [... destinations , ... eligibleOrderDestinations ( records ( nextPage ), destinationType )]
2026-08-26 11:10:42 +08:00
}
if ( receivings . length === 0 || ( destinationType !== 'PRIVATE' && destinations . length === 0 )) {
throw new Error ( '缺少创建测试订单所需的收货仓或目的仓' )
}
2026-09-23 21:58:01 +08:00
// The warehouse page can contain repeated rows for the same business code.
// Collapse those rows before applying a deterministic route offset; otherwise
// a producer intended for the second FBA destination can silently reuse ABE4.
if ( destinationType !== 'PRIVATE' ) {
destinations = [... new Map ( destinations . map ( value => [ String ( value . warehouseCode ). trim (). toUpperCase (), value ])). values ()]
2026-09-23 22:20:18 +08:00
const excluded = new Set (( fixedRoute . excludeDestinationCodes || []). map ( value => String ( value ). trim (). toUpperCase ()). filter ( Boolean ))
if ( excluded . size ) destinations = destinations . filter ( value => ! excluded . has ( String ( value . warehouseCode ). trim (). toUpperCase ()))
2026-09-23 21:58:01 +08:00
}
2026-08-08 10:11:04 +08:00
2026-08-26 11:10:42 +08:00
let pricingResult
if ( fixedRoute . originWarehouseName && ( fixedRoute . channelCode || fixedRoute . channelName )) {
const receiving = receivings . find (( warehouse ) => warehouse . warehouseName === fixedRoute . originWarehouseName )
if ( ! receiving ) throw new Error ( `未找到固定起运仓: ${ fixedRoute . originWarehouseName } ` )
const channelsResponse = await request ( context , 'GET' ,
2026-08-15 11:36:53 +08:00
`/api/merchant/v1/first-leg/pricing/channels?originWarehouseId= ${ encodeURIComponent ( receiving . id ) } ` ,
2026-08-26 11:10:42 +08:00
undefined , merchantToken )
const channels = context . apiSuccess ( MODULE , 'Client 查询固定起运仓物流渠道' , channelsResponse )
const channel = ( Array . isArray ( channels ) ? channels : []). find (( item ) =>
( ! fixedRoute . channelCode || item . channelCode === fixedRoute . channelCode )
&& ( ! fixedRoute . channelName || item . channelName === fixedRoute . channelName ))
if ( ! channel ) throw new Error ( `未找到固定物流渠道: ${ fixedRoute . channelCode || fixedRoute . channelName } ` )
const modes = Array . isArray ( channel . pricingTaxModes ) ? [... channel . pricingTaxModes ] : []
if ( modes . length === 0 ) throw new Error ( '固定物流渠道没有可用报价税制' )
const shift = modes . length ? Number ( fixedRoute . taxModeIndex || 0 ) % modes . length : 0
const selectedChannel = { ... channel , pricingTaxModes : [... modes . slice ( shift ), ... modes . slice ( 0 , shift )] }
if ( destinationType === 'PRIVATE' ) {
pricingResult = { receiving , priced : { channel : selectedChannel ,
pricingTaxMode : selectedChannel . pricingTaxModes [ 0 ], destination : null },
response : channelsResponse , attempts : 0 , failures : [], originFailures : [] }
} else {
const detailResponse = await context . request ( 'GET' ,
`/api/platform/v1/fba-pricing/carrier-rates/price-books/ ${ encodeURIComponent ( channel . priceBookVersionId ) } ` )
const detail = context . apiSuccess ( MODULE , 'Admin 查询固定渠道当前价格明细' , detailResponse )
const pricedCodes = new Set (( detail ? . rates || [])
. filter (( rate ) => rate . channelCode === channel . channelCode && rate . destinationType === destinationType )
. map (( rate ) => String ( rate . destinationCode || '' ). trim ()). filter ( Boolean ))
const eligible = destinations . filter (( destination ) => pricedCodes . has ( String ( destination . warehouseCode )))
if ( eligible . length < 1 ) throw new Error ( `固定渠道没有可计价目的仓: ${ destinationType } ` )
2026-09-23 21:58:01 +08:00
const distinctEligible = [... new Map ( eligible . map ( value => [ String ( value . warehouseCode ). trim (). toUpperCase (), value ])). values ()]
const destination = distinctEligible [ Number ( fixedRoute . destinationIndex || 0 ) % distinctEligible . length ]
2026-08-26 11:10:42 +08:00
const result = await findPricedOrderRoute ({
receiving , destinations : [ destination ], channels : [ selectedChannel ],
estimate : ( body ) => request ( context , 'POST' , '/api/merchant/v1/first-leg/pricing/estimate' , body , merchantToken ),
destinationType ,
})
pricingResult = { receiving , ... result , originFailures : [] }
}
2026-08-27 09:21:01 +08:00
} else if ( fixedRoute . originWarehouseName ) {
const receiving = receivings . find (( warehouse ) => warehouse . warehouseName === fixedRoute . originWarehouseName )
if ( ! receiving ) throw new Error ( `未找到固定起运仓: ${ fixedRoute . originWarehouseName } ` )
const channelsResponse = await request ( context , 'GET' ,
`/api/merchant/v1/first-leg/pricing/channels?originWarehouseId= ${ encodeURIComponent ( receiving . id ) } ` ,
undefined , merchantToken )
const availableChannels = context . apiSuccess ( MODULE , 'Client 查询固定起运仓随机渠道池' , channelsResponse )
const channels = rotate (( Array . isArray ( availableChannels ) ? availableChannels : [])
. filter (( item ) => Array . isArray ( item . pricingTaxModes ) && item . pricingTaxModes . length > 0 ),
fixedRoute . randomIndex )
. map (( channel , index ) => ({ ... channel ,
pricingTaxModes : rotate ( channel . pricingTaxModes , Number ( fixedRoute . randomIndex ) + index ) }))
if ( channels . length === 0 ) throw new Error ( '固定起运仓没有客户可选择的可计价渠道' )
if ( destinationType === 'PRIVATE' ) {
const channel = channels [ 0 ]
pricingResult = { receiving , priced : { channel ,
pricingTaxMode : channel . pricingTaxModes [ 0 ], destination : null },
response : channelsResponse , attempts : 0 , failures : [], originFailures : [] }
} else {
const result = await findPricedOrderRoute ({
2026-09-23 21:58:01 +08:00
receiving ,
destinations : rotate ( destinations , Number . isFinite ( Number ( fixedRoute . destinationIndex ))
? Number ( fixedRoute . destinationIndex ) : Math . floor ( Number ( fixedRoute . randomIndex ) / 17 )),
2026-08-27 09:21:01 +08:00
channels ,
estimate : ( body ) => request ( context , 'POST' , '/api/merchant/v1/first-leg/pricing/estimate' , body , merchantToken ),
destinationType ,
})
pricingResult = { receiving , ... result , originFailures : [] }
}
2026-08-26 11:10:42 +08:00
} else {
pricingResult = await findPricedOrderRouteAcrossOrigins ({
receivings , destinations ,
loadChannels : ( receiving ) => request ( context , 'GET' ,
`/api/merchant/v1/first-leg/pricing/channels?originWarehouseId= ${ encodeURIComponent ( receiving . id ) } ` ,
undefined , merchantToken ),
estimate : ( body ) => request ( context , 'POST' , '/api/merchant/v1/first-leg/pricing/estimate' , body , merchantToken ),
destinationType ,
})
}
2026-08-13 19:00:33 +08:00
const priced = pricingResult . priced
2026-08-15 11:36:53 +08:00
const receiving = pricingResult . receiving
2026-09-30 17:13:11 +08:00
context . addStep ( MODULE , destinationType === 'PRIVATE'
? 'Client 私人地址订单选择有效国内仓渠道与报价税制'
: 'Client 从 ACTIVE 国内仓选择具有真实价格覆盖的渠道和目的仓' , pricingResult . response , Boolean ( priced ),
2026-08-13 19:00:33 +08:00
priced
2026-08-26 11:10:42 +08:00
? `origin= ${ receiving . id } , priceBookVersionId= ${ priced . channel . priceBookVersionId } , channel= ${ priced . channel . channelCode } , pricingTaxMode= ${ priced . pricingTaxMode } , destination= ${ priced . destination ? . warehouseCode || 'PRIVATE_ADDRESS' } , attempts= ${ pricingResult . attempts } `
2026-08-15 11:36:53 +08:00
: originPricingFailureEvidence ( pricingResult ),
2026-08-13 19:00:33 +08:00
CONTRACT_OPTIONS )
2026-08-24 19:58:35 +08:00
if ( ! priced ) throw new Error ( `当前 ACTIVE 国内收货仓、渠道、报价税制和 ${ destinationType } 目的仓没有可计价组合` )
2026-08-08 10:11:04 +08:00
const partnerResponse = await request ( context , 'GET' ,
'/api/merchant/v1/partners/candidates?type=SHIPPER' , undefined , merchantToken )
2026-08-26 17:50:56 +08:00
const partners = context . apiSuccess ( MODULE , 'Client 查询发货客户候选' , partnerResponse , '' , CONTRACT_OPTIONS )
2026-08-08 10:11:04 +08:00
const partner = Array . isArray ( partners )
? partners . find (( item ) => item . status === 'ACTIVE' && item . types ? . includes ? .( 'SHIPPER' ))
: null
2026-08-13 19:00:33 +08:00
return { receiving , destination : priced . destination , channel : priced . channel ,
2026-08-24 19:58:35 +08:00
pricingTaxMode : priced . pricingTaxMode , destinationType , partner }
2026-08-08 10:11:04 +08:00
}
2026-09-01 21:55:27 +08:00
async function ensureCustomerAddress ( context , merchantToken , profile , addressType ) {
const path = `/api/merchant/v1/orders/first-leg/addresses?addressType= ${ encodeURIComponent ( addressType ) } `
const listResponse = await request ( context , 'GET' , path , undefined , merchantToken )
const existing = context . apiSuccess ( MODULE , `Client 查询 ${ addressType } 地址簿` , listResponse )
const existingId = id ( Array . isArray ( existing ) ? existing [ 0 ] ? . id : null )
if ( existingId ) return existingId
const isPickup = addressType === 'PICKUP'
const createResponse = await request ( context , 'POST' , '/api/merchant/v1/orders/first-leg/addresses' , {
addressType ,
addressName : isPickup ? '开发联调取货地址' : '开发联调私人目的仓' ,
companyName : profile . tenantName || `订单验收客户- ${ context . marker } ` ,
contactName : isPickup ? '订单验收发货人' : '订单验收收货人' ,
phone : isPickup ? '13800138000' : '12025550188' ,
email : isPickup ? 'order-acceptance@example.com' : 'private-receiver@example.com' ,
countryCode : isPickup ? 'CN' : 'US' ,
province : isPickup ? '广东省' : 'New York' ,
city : isPickup ? '深圳市' : 'New York' ,
district : isPickup ? '南山区' : 'Queens' ,
addressLine : ` ${ context . marker } ${ isPickup ? '开发联调取货地址' : '88 Randomized Test Ave' } ` ,
postalCode : isPickup ? '518000' : '11354' ,
defaultAddress : true ,
}, merchantToken )
return id ( context . apiSuccess ( MODULE , `Client 创建 ${ addressType } 地址` , createResponse ,
`addressType= ${ addressType } ` , CONTRACT_OPTIONS ))
}
function buildPayload ( context , profile , master , productImageFileId , customerAddresses ) {
2026-08-08 10:11:04 +08:00
const shipmentId = `SHIP- ${ context . marker } ` . slice ( 0 , 64 )
const referenceId = `REF- ${ context . marker } ` . slice ( 0 , 64 )
const companyName = profile . tenantName || `订单验收客户- ${ context . marker } `
2026-08-26 11:10:42 +08:00
const requestedSkuCount = Number ( context . context ? . waitSplitVariant ? . skuCount || BOXES . length )
const skuCount = Math . max ( 1 , Math . min ( BOXES . length , requestedSkuCount ))
const packageCountPerSku = Math . max ( 1 ,
Number ( context . context ? . waitSplitVariant ? . packageCountPerSku
|| context . context ? . phase3LoadingIncrementalPackageCount || 1 ))
2026-08-27 09:21:01 +08:00
const packageCounts = Array . isArray ( context . context ? . waitSplitVariant ? . packageCounts )
? context . context . waitSplitVariant . packageCounts : []
2026-08-26 11:10:42 +08:00
const selectedBoxes = BOXES . slice ( 0 , skuCount )
const orderProfile = context . context ? . orderProfile || {}
2026-09-11 16:17:16 +08:00
const dropship = String ( context . context ? . orderFulfillmentType || 'STANDARD' ). trim (). toUpperCase ()
=== 'ONE_CLICK_DROPSHIP'
const mixedPackageCount = Math . max ( 1 ,
Number ( context . context ? . waitSplitVariant ? . totalPackageCount || packageCountPerSku ))
const mixedPackingGroupNo = `MIX- ${ context . marker } ` . slice ( 0 , 32 )
2026-08-26 11:10:42 +08:00
const isPrivate = master . destinationType === 'PRIVATE'
2026-09-02 12:19:46 +08:00
const pickupMethod = orderProfile . pickupMethod || 'SELF_EXPRESS'
2026-08-26 11:10:42 +08:00
const receiver = isPrivate ? {
companyName : ` ${ companyName } 私人收件人` , contactName : `收件人- ${ context . marker } ` . slice ( 0 , 64 ),
phone : '12025550188' , email : 'private-receiver@example.com' , countryCode : 'US' ,
province : 'New York' , city : 'New York' , district : 'Queens' ,
addressLine : ` ${ context . marker } 88 Randomized Test Ave` , postalCode : '11354' ,
} : null
2026-08-08 10:11:04 +08:00
return {
2026-09-11 16:17:16 +08:00
fulfillmentType : dropship ? 'ONE_CLICK_DROPSHIP' : 'STANDARD' ,
2026-08-26 11:10:42 +08:00
customerId : profile . id ,
customerName : companyName ,
2026-08-08 10:11:04 +08:00
transportMode : 'SEA' , tradeTerm : 'FOB' ,
2026-08-26 11:10:42 +08:00
destinationCountryCode : master . destination ? . countryCode || receiver ? . countryCode || 'US' ,
destinationWarehouseId : master . destination ? . id || null ,
destinationWarehouseName : master . destination ? . warehouseName || ( isPrivate ? '私人地址' : null ),
2026-08-08 10:11:04 +08:00
expectedShipDate : tomorrow (), customerRemark : ` ${ context . marker } Client 创建隔离订单` ,
2026-08-26 11:10:42 +08:00
pickupMethod , requestedPickupAt : pickupMethod === 'PICKUP'
? new Date ( Date . now () + 7 * 86_400_000 ). toISOString (). slice ( 0 , 19 ) : null ,
2026-09-01 21:55:27 +08:00
senderAddressId : pickupMethod === 'PICKUP' ? customerAddresses . pickupAddressId : null ,
privateDestinationAddressId : isPrivate ? customerAddresses . privateDestinationAddressId : null ,
2026-08-26 11:10:42 +08:00
receivingWarehouseId : master . receiving . id ,
2026-08-08 10:11:04 +08:00
receivingWarehouseName : master . receiving . warehouseName ,
channelCode : master . channel . channelCode , channelName : master . channel . channelName ,
2026-08-26 11:10:42 +08:00
carrierId : null , carrierName : null , taxMode : orderProfile . taxMode || 'BUY_DECLARATION' ,
pricingTaxMode : master . pricingTaxMode , containerMode : orderProfile . containerMode || 'LCL' ,
customsInspection : orderProfile . customsInspection ?? false , destinationType : master . destinationType ,
amazonWarehouseCode : master . destinationType === 'FBA' ? master . destination ? . warehouseCode : null ,
2026-08-24 19:58:35 +08:00
shipmentId : master . destinationType === 'FBA' ? shipmentId : null ,
referenceId : master . destinationType === 'FBA' ? referenceId : null ,
2026-08-08 10:11:04 +08:00
sender : {
companyName , contactName : '订单验收联系人' , phone : '13800138000' ,
email : 'order-acceptance@example.com' , countryCode : 'CN' , province : '广东省' ,
city : '深圳市' , district : '南山区' , addressLine : ` ${ context . marker } 测试地址` , postalCode : '518000' ,
},
2026-08-26 11:10:42 +08:00
receiver ,
cargoes : selectedBoxes . map (( box , index ) => {
2026-08-08 10:11:04 +08:00
const unitsPerPackage = 10 + index
2026-09-11 16:17:16 +08:00
const packageCount = dropship ? mixedPackageCount : Number ( packageCounts [ index ] || packageCountPerSku )
const carton = dropship ? selectedBoxes [ 0 ] : box
const cartonVolumeCbm = carton . lengthCm * carton . widthCm * carton . heightCm / 1_000_000
2026-08-08 10:11:04 +08:00
return {
2026-09-15 10:37:34 +08:00
skuNo : context . context ? . waitSplitVariant ? . skuCodes ? .[ index ]
|| `SKU- ${ box . boxType } - ${ context . marker } ` . slice ( 0 , 64 ),
2026-08-08 10:11:04 +08:00
cargoName : ` ${ box . boxType } 测试产品` , cargoNameEn : ` ${ box . boxType } test product` ,
hsCode : '8518210000' , customsHsCode : '8518210000' , packageType : 'CARTON' ,
2026-09-11 16:17:16 +08:00
packingGroupNo : dropship ? mixedPackingGroupNo : null ,
lengthCm : carton . lengthCm , widthCm : carton . widthCm , heightCm : carton . heightCm ,
2026-09-11 16:50:10 +08:00
unitsPerPackage , packageCount , grossWeightKg : ( dropship ? 10 : 10 + index ) * packageCount ,
2026-09-11 16:17:16 +08:00
volumeCbm : cartonVolumeCbm * packageCount , totalQuantity : unitsPerPackage * packageCount ,
2026-09-11 16:50:10 +08:00
netWeightKg : ( dropship ? 9 : 9 + index ) * packageCount ,
2026-08-17 15:41:42 +08:00
unitDeclaredValue : 8.5 , declaredValue : unitsPerPackage * packageCount * 8.5 , currency : 'USD' ,
2026-08-08 10:11:04 +08:00
material : '塑料 / Plastic' , usage : '接口验收 / API acceptance' ,
model : `MODEL- ${ box . boxType } ` , brand : 'TEST-NO-LOGO' ,
productLengthCm : 12 + index , productWidthCm : 8 + index , productHeightCm : 6 + index ,
2026-08-24 19:58:35 +08:00
shipmentId : master . destinationType === 'FBA' ? shipmentId : null ,
referenceId : master . destinationType === 'FBA' ? referenceId : null ,
upcNo : ` ${ Date . now () }${ index } ` . slice ( - 12 ),
2026-09-30 17:13:11 +08:00
productImageFileId , dangerousGoods : orderProfile . dangerousGoods === true ,
woodenProduct : orderProfile . woodenProduct === true ,
2026-08-08 10:11:04 +08:00
cargoRemark : ` ${ context . marker } boxType= ${ box . boxType } ` ,
}
}),
}
}
function assertCargoContract ( context , response , cargoes , payload ) {
2026-08-26 11:10:42 +08:00
const expectedCargoes = payload . cargoes || []
const complete = Array . isArray ( cargoes ) && cargoes . length === expectedCargoes . length
&& expectedCargoes . every (( expected , index ) => {
const actual = cargoes [ index ]
return Number ( actual ? . lengthCm ) === expected . lengthCm && Number ( actual ? . widthCm ) === expected . widthCm
&& Number ( actual ? . heightCm ) === expected . heightCm && id ( actual ? . productImageFileId ) === id ( expected . productImageFileId )
2026-08-08 10:11:04 +08:00
&& actual ? . skuNo === expected . skuNo && actual ? . cargoName === expected . cargoName
&& actual ? . cargoNameEn === expected . cargoNameEn && actual ? . customsHsCode === expected . customsHsCode
&& Number ( actual ? . unitsPerPackage ) === expected . unitsPerPackage
2026-08-17 15:41:42 +08:00
&& Number ( actual ? . packageCount ) === expected . packageCount && Number ( actual ? . totalQuantity ) === expected . totalQuantity
2026-08-08 10:11:04 +08:00
&& Number ( actual ? . netWeightKg ) === expected . netWeightKg
&& Number ( actual ? . unitDeclaredValue ) === expected . unitDeclaredValue
&& actual ? . material === expected . material && actual ? . productUsage === expected . usage
&& actual ? . model === expected . model && actual ? . brand === expected . brand
&& Number ( actual ? . productLengthCm ) === expected . productLengthCm
&& Number ( actual ? . productWidthCm ) === expected . productWidthCm
&& Number ( actual ? . productHeightCm ) === expected . productHeightCm
2026-08-24 21:00:22 +08:00
&& ( actual ? . shipmentId ?? null ) === ( expected . shipmentId ?? null )
&& ( actual ? . referenceId ?? null ) === ( expected . referenceId ?? null )
2026-08-08 10:11:04 +08:00
&& actual ? . upcNo === expected . upcNo && actual ? . cargoRemark . includes ( context . marker )
})
2026-08-26 11:10:42 +08:00
context . assert ( MODULE , '动态SKU产品字段及图片契约完整回显' , response , complete ,
2026-08-08 10:11:04 +08:00
`cargoes= ${ Array . isArray ( cargoes ) ? cargoes . length : 'n/a' } , imageFileId= ${ payload . cargoes [ 0 ]. productImageFileId } ` ,
CONTRACT_OPTIONS )
}
async function assertDimensionConfig ( context , tenantId ) {
const path = `/api/platform/v1/first-leg/customer-dimension-config?tenantId= ${ encodeURIComponent ( tenantId ) } `
const response = await context . request ( 'GET' , path )
const data = context . apiSuccess ( MODULE , 'Admin 查询客户固定箱型' , response , `tenantId= ${ tenantId } ` , CONTRACT_OPTIONS )
const rules = Array . isArray ( data ? . rules ) ? data . rules : []
const exact = BOXES . every (( box , index ) => {
const rule = rules [ index ]
return rule ? . boxType === box . boxType && Number ( rule . innerLengthCm ) === box . lengthCm
&& Number ( rule . innerWidthCm ) === box . widthCm && Number ( rule . innerHeightCm ) === box . heightCm
&& Number ( rule . edgeAdditionCm ) === 2
})
2026-09-03 16:09:22 +08:00
context . assert ( MODULE , '固定四箱型顺序、内径及各尺寸增加2cm正确' , response ,
2026-08-08 10:11:04 +08:00
rules . length === BOXES . length && exact ,
`types= ${ rules . map (( rule ) => rule . boxType ). join ( ',' ) || 'missing' } ` , CONTRACT_OPTIONS )
}
async function waitForInbound ( context , tenantId , orderId ) {
const path = `/api/platform/v1/orders/ ${ encodeURIComponent ( orderId ) } ?tenantId= ${ encodeURIComponent ( tenantId ) } `
2026-08-13 18:34:44 +08:00
const taskPath = `/api/platform/v1/first-leg/tasks/by-order/ ${ encodeURIComponent ( orderId ) } ?tenantId= ${ encodeURIComponent ( tenantId ) } `
let response = null ; let detail = null ; let taskResponse = null ; let task = null
for ( let attempt = 1 ; attempt <= 30 ; attempt += 1 ) {
2026-08-08 10:11:04 +08:00
response = await context . request ( 'GET' , path )
2026-08-13 18:34:44 +08:00
const candidate = success ( response ) ? response . body ? . data : null
if ( candidate ? . summary ? . inboundId ) {
taskResponse = await context . request ( 'GET' , taskPath )
}
if ( approvedInboundReady ( candidate , taskResponse , tenantId , orderId )) {
detail = candidate
task = taskResponse . body . data
2026-08-08 10:11:04 +08:00
break
}
2026-08-13 18:34:44 +08:00
if ( attempt < 30 ) await sleep ( 1_000 )
2026-08-08 10:11:04 +08:00
}
2026-08-13 18:34:44 +08:00
context . addStep ( MODULE , '审核后入库单与履约任务异步收敛' , response , Boolean ( detail && task ),
detail
? `inboundId= ${ detail . summary . inboundId } , status= ${ detail . summary . mainStatus } , taskId= ${ id ( task ? . id ) } `
: `30秒内未收敛;status= ${ response ? . body ? . data ? . summary ? . mainStatus ?? 'missing' } , taskCode= ${ taskResponse ? . body ? . code ?? 'not-requested' } ` ,
CONTRACT_OPTIONS )
return { response , detail , taskResponse , task }
2026-08-08 10:11:04 +08:00
}
export async function run ( context ) {
2026-10-03 05:25:30 +08:00
// Resolve per invocation: this module is cached across serial fresh-customer runs.
const fixture = context . context ? . merchantQualificationFixture
const configured = context . options ? . merchantUsername || context . options ? . merchantPassword
const credentials = fixture ? { username : fixture . username , password : fixture . password }
: configured ? { username : context . options . merchantUsername , password : context . options . merchantPassword }
: { username : process . env . MERCHANT_TEST_USERNAME , password : process . env . MERCHANT_TEST_PASSWORD }
if ( ! credentials . username || ! credentials . password ) throw new Error ( 'Client 当前运行缺少完整登录凭据' )
2026-08-08 10:11:04 +08:00
const loginResponse = await context . request ( 'POST' , '/api/auth/login' , {
2026-10-03 05:25:30 +08:00
loginId : credentials . username , password : credentials . password ,
2026-08-08 10:11:04 +08:00
}, '' )
const login = context . apiSuccess ( MODULE , 'Client 测试账号登录' , loginResponse ,
2026-10-03 05:25:30 +08:00
'current-run customer credentials' )
2026-08-08 10:11:04 +08:00
const merchantToken = login ? . accessToken
if ( ! merchantToken ) throw new Error ( 'Client 登录未返回 accessToken' )
const profileResponse = await request ( context , 'GET' , '/api/merchant/v1/iam/tenant/profile' ,
undefined , merchantToken )
const profile = context . apiSuccess ( MODULE , 'Client 查询当前企业资料' , profileResponse )
const tenantId = id ( profile ? . id )
context . assert ( MODULE , 'Client 测试企业已审核且客户 ID 有效' , profileResponse ,
Boolean ( tenantId ) && profile ? . status === 'ACTIVE' , `tenantId= ${ tenantId || 'missing' } , status= ${ profile ? . status ?? 'missing' } ` )
if ( ! tenantId ) throw new Error ( 'Client 企业资料未返回客户 ID' )
2026-08-27 09:21:01 +08:00
if ( context . options ? . customerName ) {
context . assert ( MODULE , 'Client 登录企业与指定客户名称一致' , profileResponse ,
profile ? . tenantName === context . options . customerName ,
`expected= ${ context . options . customerName } , actual= ${ profile ? . tenantName || 'missing' } ` , CONTRACT_OPTIONS )
if ( profile ? . tenantName !== context . options . customerName ) throw new Error ( 'Client 登录企业不是指定客户' )
}
2026-08-08 10:11:04 +08:00
await assertDimensionConfig ( context , tenantId )
const master = await loadMasterData ( context , merchantToken )
const productImageFileId = await uploadProductImage ( context , merchantToken )
if ( ! productImageFileId ) throw new Error ( '产品图片新契约未部署,无法创建带图片的隔离订单' )
await assertProductImageDownload ( context , merchantToken , productImageFileId )
2026-09-01 21:55:27 +08:00
const customerAddresses = {
pickupAddressId : await ensureCustomerAddress ( context , merchantToken , profile , 'PICKUP' ),
privateDestinationAddressId : master . destinationType === 'PRIVATE'
? await ensureCustomerAddress ( context , merchantToken , profile , 'PRIVATE_DESTINATION' ) : null ,
}
2026-08-08 10:11:04 +08:00
const createPath = '/api/merchant/v1/orders/first-leg'
2026-09-15 12:07:43 +08:00
const policyResponse = await request ( context , 'GET' , ` ${ createPath } /inbound-code-policy` , undefined , merchantToken )
const policy = context . apiSuccess ( MODULE , 'Client 读取入仓码录入策略' , policyResponse , '' , CONTRACT_OPTIONS )
const payload = { ... buildPayload ( context , profile , master , productImageFileId , customerAddresses ),
... fixtureInboundCode ( policy , context . marker ) }
2026-09-30 17:13:11 +08:00
if ( context . context . verifyOrderCargoNegatives === true ) {
await verifyCargoRequirementNegatives ( context , payload , policy , merchantToken )
}
2026-08-08 10:11:04 +08:00
const createResponse = await request ( context , 'POST' , createPath , payload , merchantToken ,
{ 'Idempotency-Key' : key ( context , 'client-create' ) })
const orderId = id ( context . apiSuccess ( MODULE , 'Client 创建四箱型隔离订单' , createResponse ,
`marker= ${ context . marker } ` , CONTRACT_OPTIONS ))
if ( ! orderId ) throw new Error ( 'Client 订单创建失败,后续 Admin 链路停止' )
context . createdResources . orders = {
marker : context . marker , tenantId , orderId , productImageFileId ,
2026-08-25 08:40:27 +08:00
fulfillmentType : payload . fulfillmentType ,
2026-08-25 09:23:28 +08:00
skuCodes : payload . cargoes . map (( cargo ) => cargo . skuNo ),
skuLines : payload . cargoes . map (( cargo ) => ({
skuCode : cargo . skuNo , packageCount : cargo . packageCount ,
})),
2026-08-08 10:11:04 +08:00
boxTypes : BOXES . map (( box ) => box . boxType ), inboundId : null ,
2026-08-12 13:24:17 +08:00
receivingWarehouseId : id ( master . receiving . id ),
2026-08-26 11:10:42 +08:00
receivingWarehouseName : master . receiving . warehouseName ,
2026-08-24 19:58:35 +08:00
destinationType : master . destinationType ,
2026-08-26 11:10:42 +08:00
destinationWarehouseId : id ( master . destination ? . id ),
destinationWarehouseCode : master . destination ? . warehouseCode || null ,
destinationWarehouseName : master . destination ? . warehouseName || ( master . destinationType === 'PRIVATE' ? '私人地址' : null ),
channelCode : master . channel . channelCode ,
channelName : master . channel . channelName ,
pricingTaxMode : master . pricingTaxMode ,
pickupMethod : payload . pickupMethod ,
taxMode : payload . taxMode ,
containerMode : payload . containerMode ,
customsInspection : payload . customsInspection ,
2026-08-08 10:11:04 +08:00
}
const replayResponse = await request ( context , 'POST' , createPath , payload , merchantToken ,
{ 'Idempotency-Key' : key ( context , 'client-create' ) })
const replayedId = id ( context . apiSuccess ( MODULE , 'Client 创建订单幂等重放' , replayResponse ))
context . assert ( MODULE , 'Client 创建幂等结果一致' , replayResponse , replayedId === orderId ,
`orderId= ${ orderId } , replayedId= ${ replayedId } ` )
const clientDetailPath = `/api/merchant/v1/orders/first-leg/ ${ encodeURIComponent ( orderId ) } `
const draftResponse = await request ( context , 'GET' , clientDetailPath , undefined , merchantToken )
2026-10-02 23:14:24 +08:00
let draft = context . apiSuccess ( MODULE , 'Client 查询隔离订单草稿' , draftResponse )
2026-08-08 10:11:04 +08:00
const orderNo = id ( draft ? . order ? . orderNo )
context . assert ( MODULE , 'Client 草稿身份、marker 和状态正确' , draftResponse ,
id ( draft ? . order ? . id ) === orderId && draft ? . order ? . status === 'DRAFT'
&& String ( draft ? . order ? . customerRemark || '' ). includes ( context . marker ),
`orderNo= ${ orderNo || 'missing' } , status= ${ draft ? . order ? . status ?? 'missing' } ` )
assertCargoContract ( context , draftResponse , draft ? . cargoes , payload )
context . createdResources . orders . orderNo = orderNo
2026-10-02 23:14:24 +08:00
draft = await verifyPlatformDraftEdit ( context , tenantId , orderId , merchantToken , payload )
2026-10-02 23:37:15 +08:00
await verifyPlatformSubmitCancel ( context , tenantId , payload , policy )
2026-08-08 10:11:04 +08:00
2026-09-01 12:02:35 +08:00
const createConflictPayload = {
... payload ,
customerRemark : ` ${ payload . customerRemark } 同键异载荷` ,
}
const createConflictResponse = await request ( context , 'POST' , createPath , createConflictPayload , merchantToken ,
{ 'Idempotency-Key' : key ( context , 'client-create' ) })
context . assert ( MODULE , 'Client 创建订单同键异载荷返回A0409' , createConflictResponse ,
createConflictResponse . http === 200 && String ( createConflictResponse . body ? . code ) === 'A0409' ,
`http= ${ createConflictResponse . http } , code= ${ createConflictResponse . body ? . code ?? 'missing' } ` , CONTRACT_OPTIONS )
const draftAfterConflictResponse = await request ( context , 'GET' , clientDetailPath , undefined , merchantToken )
const draftAfterConflict = context . apiSuccess ( MODULE , 'Client 回查创建冲突后的订单草稿' , draftAfterConflictResponse )
context . assert ( MODULE , '创建同键异载荷失败后订单全事实零漂移' , draftAfterConflictResponse ,
orderPersistenceFacts ( draftAfterConflict ) === orderPersistenceFacts ( draft ),
`orderId= ${ orderId } , status= ${ draftAfterConflict ? . order ? . status ?? 'missing' } ` , CONTRACT_OPTIONS )
2026-10-02 23:14:24 +08:00
const submitCommand = freezeOrderCommand ( draft , key ( context , 'client-submit' ))
context . createdResources . orders . submitCommand = submitCommand
const submitResponse = await requestTransportRetry ( context , 'POST' , ` ${ clientDetailPath } /submit` , submitCommand . body ,
merchantToken , submitCommand . headers )
2026-08-08 10:11:04 +08:00
context . apiSuccess ( MODULE , 'Client 提交隔离订单' , submitResponse , `orderId= ${ orderId } ` )
2026-10-02 23:14:24 +08:00
const submitReplayResponse = await request ( context , 'POST' , ` ${ clientDetailPath } /submit` , submitCommand . body ,
merchantToken , submitCommand . headers )
2026-09-01 12:02:35 +08:00
context . apiSuccess ( MODULE , 'Client 提交订单同键同载荷精确重放' , submitReplayResponse , `orderId= ${ orderId } ` )
2026-08-08 10:11:04 +08:00
const submittedResponse = await request ( context , 'GET' , clientDetailPath , undefined , merchantToken )
const submitted = context . apiSuccess ( MODULE , 'Client 回查已提交订单' , submittedResponse )
context . assert ( MODULE , 'Client 订单已进入待审核状态' , submittedResponse ,
submitted ? . order ? . status === 'SUBMITTED' , `status= ${ submitted ? . order ? . status ?? 'missing' } ` )
2026-10-02 23:14:24 +08:00
const conflictSubmitResponse = await request ( context , 'POST' , ` ${ clientDetailPath } /submit` , { expectedVersion : submitted . order . version },
merchantToken , submitCommand . headers )
context . assert ( MODULE , '提交同键异版本返回A0409' , conflictSubmitResponse ,
conflictSubmitResponse . http === 200 && String ( conflictSubmitResponse . body ? . code ) === 'A0409' , 'frozen-key/different-payload' , CONTRACT_OPTIONS )
const duplicateCommand = freezeOrderCommand ( submitted , key ( context , 'client-submit-new-key' ))
const duplicateSubmitResponse = await request ( context , 'POST' , ` ${ clientDetailPath } /submit` , duplicateCommand . body ,
merchantToken , duplicateCommand . headers )
2026-09-01 12:02:35 +08:00
context . assert ( MODULE , '已提交订单使用新键重复提交返回A0409' , duplicateSubmitResponse ,
duplicateSubmitResponse . http === 200 && String ( duplicateSubmitResponse . body ? . code ) === 'A0409' ,
`http= ${ duplicateSubmitResponse . http } , code= ${ duplicateSubmitResponse . body ? . code ?? 'missing' } ` , CONTRACT_OPTIONS )
const submittedAfterFailureResponse = await request ( context , 'GET' , clientDetailPath , undefined , merchantToken )
const submittedAfterFailure = context . apiSuccess ( MODULE , 'Client 回查重复提交失败后的订单' , submittedAfterFailureResponse )
context . assert ( MODULE , '重复提交失败后订单状态货物与历史零漂移' , submittedAfterFailureResponse ,
orderPersistenceFacts ( submittedAfterFailure ) === orderPersistenceFacts ( submitted ),
`orderId= ${ orderId } , status= ${ submittedAfterFailure ? . order ? . status ?? 'missing' } ` , CONTRACT_OPTIONS )
2026-08-08 10:11:04 +08:00
const unifiedListResponse = await context . request ( 'GET' ,
`/api/platform/v1/orders?page=1&size=100&tenantId= ${ encodeURIComponent ( tenantId ) } &orderNo= ${ encodeURIComponent ( orderNo ) } ` )
const unifiedPage = context . apiSuccess ( MODULE , 'Admin 查询统一订单列表' , unifiedListResponse )
const unifiedRow = records ( unifiedPage ). find (( row ) => id ( row . orderId ) === orderId )
context . assert ( MODULE , 'Admin 统一订单列表只核对本轮订单' , unifiedListResponse ,
pageOk ( unifiedPage ) && Boolean ( unifiedRow ), `orderId= ${ orderId } , found= ${ Boolean ( unifiedRow ) } ` )
const unifiedDetailPath = `/api/platform/v1/orders/ ${ encodeURIComponent ( orderId ) } ?tenantId= ${ encodeURIComponent ( tenantId ) } `
const unifiedDetailResponse = await context . request ( 'GET' , unifiedDetailPath )
const unifiedDetail = context . apiSuccess ( MODULE , 'Admin 查询统一订单详情' , unifiedDetailResponse )
context . assert ( MODULE , 'Admin 统一订单详情身份与待审核状态正确' , unifiedDetailResponse ,
id ( unifiedDetail ? . summary ? . orderId ) === orderId && unifiedDetail ? . summary ? . mainStatus === 'PENDING_REVIEW'
&& Array . isArray ( unifiedDetail ? . timeline ),
`mainStatus= ${ unifiedDetail ? . summary ? . mainStatus ?? 'missing' } , timeline= ${ unifiedDetail ? . timeline ? . length ?? 'n/a' } ` )
const firstLegListResponse = await context . request ( 'GET' ,
`/api/platform/v1/orders/first-leg?tenantId= ${ encodeURIComponent ( tenantId ) } &keyword= ${ encodeURIComponent ( orderNo ) } &page=1&size=100` )
const firstLegPage = context . apiSuccess ( MODULE , 'Admin 查询头程订单列表' , firstLegListResponse )
const firstLegRow = records ( firstLegPage ). find (( row ) => id ( row . id ) === orderId )
context . assert ( MODULE , 'Admin 头程订单列表只核对本轮订单' , firstLegListResponse ,
pageOk ( firstLegPage ) && Boolean ( firstLegRow ), `orderId= ${ orderId } , found= ${ Boolean ( firstLegRow ) } ` )
const firstLegDetailPath = `/api/platform/v1/orders/first-leg/ ${ encodeURIComponent ( orderId ) } ?tenantId= ${ encodeURIComponent ( tenantId ) } `
const firstLegDetailResponse = await context . request ( 'GET' , firstLegDetailPath )
const firstLegDetail = context . apiSuccess ( MODULE , 'Admin 查询头程订单详情' , firstLegDetailResponse )
context . assert ( MODULE , 'Admin 头程订单详情聚合结构完整' , firstLegDetailResponse ,
id ( firstLegDetail ? . order ? . id ) === orderId && Array . isArray ( firstLegDetail ? . cargoes )
&& Array . isArray ( firstLegDetail ? . parties ) && Array . isArray ( firstLegDetail ? . internalNotes )
&& Array . isArray ( firstLegDetail ? . statusHistory ),
`cargoes= ${ firstLegDetail ? . cargoes ? . length ?? 'n/a' } , history= ${ firstLegDetail ? . statusHistory ? . length ?? 'n/a' } ` )
assertCargoContract ( context , firstLegDetailResponse , firstLegDetail ? . cargoes , payload )
2026-09-14 17:29:03 +08:00
const noteResponse = await context . request ( 'PUT' ,
2026-09-14 17:31:06 +08:00
`/api/platform/v1/orders/ ${ encodeURIComponent ( orderId ) } /remarks?tenantId= ${ encodeURIComponent ( tenantId ) } ` ,
2026-09-14 17:29:03 +08:00
{
expectedVersion : Number ( firstLegDetail ? . order ? . version ),
internalRemark : ` ${ context . marker } Admin 订单链路验收` ,
warehouseRemark : firstLegDetail ? . order ? . warehouseRemark || null ,
customerServiceRemark : firstLegDetail ? . order ? . customerServiceRemark || null ,
})
2026-08-08 10:11:04 +08:00
context . apiSuccess ( MODULE , 'Admin 为本轮订单添加内部备注' , noteResponse )
const approveResponse = await context . request ( 'POST' ,
`/api/platform/v1/orders/first-leg/ ${ encodeURIComponent ( orderId ) } /approve?tenantId= ${ encodeURIComponent ( tenantId ) } ` )
context . apiSuccess ( MODULE , 'Admin 审核通过本轮订单' , approveResponse , `orderId= ${ orderId } ` )
2026-09-03 16:09:22 +08:00
// APPROVED declaration edits fail closed against the authoritative Firstleg projection.
// Wait for the approval outbox to create that projection before exercising the edit API.
const inboundResult = await waitForInbound ( context , tenantId , orderId )
const inboundId = id ( inboundResult . detail ? . summary ? . inboundId )
if ( ! inboundId || ! inboundResult . task ) {
throw new Error ( '订单审核后的入库单与履约任务未在30秒内收敛,无法修改申报并向 OCR/墨家模块交接' )
}
2026-09-30 15:16:39 +08:00
await verifyOrderApprovalReplay ( context , { tenantId , orderId , inboundId ,
inboundCode : id ( inboundResult . detail ? . summary ? . inboundCode ) })
2026-09-02 15:59:13 +08:00
const editableDetailResponse = await context . request ( 'GET' , firstLegDetailPath )
const editableDetail = context . apiSuccess ( MODULE , 'Admin 读取审核后客户申报资料' , editableDetailResponse )
const declarationDetail = editableDetail ? . order ? . order ? editableDetail . order : editableDetail
const declarationOrder = declarationDetail ? . order || {}
const declarationCargoes = Array . isArray ( declarationDetail ? . cargoes ) ? declarationDetail . cargoes : []
const declarationVersion = Number ( declarationOrder ? . version )
const declarationRemark = ` ${ context . marker } Admin 修改客户申报资料`
const declarationPayload = {
expectedVersion : declarationVersion ,
customerRemark : declarationRemark ,
expectedArrivalAt : declarationOrder ? . expectedArrivalAt || editableDetail ? . summary ? . expectedArrivalAt || null ,
channelCode : declarationOrder ? . channelCode || editableDetail ? . summary ? . channelCode ,
channelName : declarationOrder ? . channelName || editableDetail ? . summary ? . channelName ,
cargoes : declarationCargoes . map ( declarationCargo ),
}
const declarationUpdateResponse = await context . request ( 'PUT' ,
`/api/platform/v1/orders/ ${ encodeURIComponent ( orderId ) } /declaration?tenantId= ${ encodeURIComponent ( tenantId ) } ` ,
declarationPayload )
const declarationUpdate = context . apiSuccess ( MODULE , 'Admin 修改审核后客户申报资料' , declarationUpdateResponse )
context . assert ( MODULE , '申报修改保持订单号入仓码和渠道且版本递增' , declarationUpdateResponse ,
Number . isFinite ( declarationVersion ) && Number ( declarationUpdate ? . version ) === declarationVersion + 1
&& id ( declarationUpdate ? . inboundCode ) === id ( firstLegDetail ? . order ? . inboundCode )
&& declarationUpdate ? . channelChanged === false ,
`version= ${ declarationVersion } -> ${ declarationUpdate ? . version ?? 'missing' } , inboundCode= ${ declarationUpdate ? . inboundCode ?? 'missing' } ` ,
CONTRACT_OPTIONS )
const staleDeclarationResponse = await context . request ( 'PUT' ,
`/api/platform/v1/orders/ ${ encodeURIComponent ( orderId ) } /declaration?tenantId= ${ encodeURIComponent ( tenantId ) } ` ,
{ ... declarationPayload , customerRemark : ` ${ declarationRemark } stale` })
context . assert ( MODULE , '申报资料旧版本并发修改返回A0409' , staleDeclarationResponse ,
staleDeclarationResponse . http === 200 && String ( staleDeclarationResponse . body ? . code ) === 'A0409' ,
`code= ${ staleDeclarationResponse . body ? . code ?? 'missing' } ` , CONTRACT_OPTIONS )
2026-08-08 10:11:04 +08:00
const inboundPath = `/api/platform/inbounds/ ${ encodeURIComponent ( inboundId ) } ?tenantId= ${ encodeURIComponent ( tenantId ) } `
const inboundResponse = await context . request ( 'GET' , inboundPath )
const inbound = context . apiSuccess ( MODULE , 'Admin 查询本轮入库单详情' , inboundResponse ,
`inboundId= ${ inboundId } ` , CONTRACT_OPTIONS )
const markerValues = [
inbound ? . publicRemark , inbound ? . summary ? . orderNo , inbound ? . summary ? . inboundCode ,
...( Array . isArray ( inbound ? . internalNotes ) ? inbound . internalNotes . map (( note ) => note . content ) : []),
]
const markerFound = markerValues . some (( value ) => String ( value || '' ). includes ( context . marker ))
const measurable = MEASURABLE_STATUSES . has ( inbound ? . summary ? . processStatus )
context . assert ( MODULE , '隔离入库单 marker 来源与可测量状态正确' , inboundResponse ,
id ( inbound ? . summary ? . id ) === inboundId && id ( inbound ? . summary ? . tenantId ) === tenantId
&& markerFound && measurable ,
`markerFound= ${ markerFound } , status= ${ inbound ? . summary ? . processStatus ?? 'missing' } ` , CONTRACT_OPTIONS )
context . assert ( MODULE , '入库单详情包含托盘库位、文件、备注和状态历史' , inboundResponse ,
Array . isArray ( inbound ? . palletLocations ) && Array . isArray ( inbound ? . files )
&& Array . isArray ( inbound ? . internalNotes ) && Array . isArray ( inbound ? . statusHistory ),
`inboundId= ${ inboundId } , history= ${ inbound ? . statusHistory ? . length ?? 'n/a' } ` )
const inboundListResponse = await context . request ( 'GET' ,
`/api/platform/inbounds?tenantId= ${ encodeURIComponent ( tenantId ) } &inboundCode= ${ encodeURIComponent ( inbound . summary . inboundCode ) } &page=1&size=100` )
const inboundPage = context . apiSuccess ( MODULE , 'Admin 查询入库单列表' , inboundListResponse )
context . assert ( MODULE , 'Admin 入库单列表只核对本轮入库单' , inboundListResponse ,
pageOk ( inboundPage ) && records ( inboundPage ). some (( row ) => id ( row . id ) === inboundId ),
`inboundId= ${ inboundId } , total= ${ inboundPage ? . total ?? 'n/a' } ` )
2026-09-02 12:19:46 +08:00
let declarationConverged = false
let convergedInboundResponse = inboundResponse
for ( let attempt = 0 ; attempt < 30 ; attempt += 1 ) {
convergedInboundResponse = await context . request ( 'GET' , inboundPath )
const currentInbound = success ( convergedInboundResponse ) ? convergedInboundResponse . body ? . data : null
if ( currentInbound ? . publicRemark === declarationRemark ) { declarationConverged = true ; break }
if ( attempt < 29 ) await sleep ( 1_000 )
}
context . assert ( MODULE , '申报修改异步同步到Firstleg入库事实' , convergedInboundResponse ,
declarationConverged , `expectedRemark= ${ declarationRemark } ` , CONTRACT_OPTIONS )
2026-08-08 10:11:04 +08:00
context . createdResources . orders . inboundId = inboundId
context . createdResources . orders . inboundCode = inbound . summary . inboundCode
2026-09-02 12:19:46 +08:00
context . createdResources . orders . customerRemark = declarationRemark
2026-08-18 16:39:39 +08:00
context . createdResources . orders . internalRemark = ` ${ context . marker } Admin 订单链路验收`
context . createdResources . orders . cargoRemark = payload . cargoes ? .[ 0 ] ? . cargoRemark || null
2026-08-08 10:11:04 +08:00
context . createdResources . orders . processStatus = inbound . summary . processStatus
context . createdResources . ocrInbound = { tenantId , inboundId , marker : context . marker }
context . createdResources . testInbound = { tenantId , inboundId , marker : context . marker }
context . context . inboundWeightOcr = { tenantId , inboundId , marker : context . marker }
context . context . ocrInbound = context . context . inboundWeightOcr
context . context . testInbound = context . context . inboundWeightOcr
context . context . createdInbound = context . context . inboundWeightOcr
2026-08-13 18:34:44 +08:00
const taskResponse = inboundResult . taskResponse
const task = inboundResult . task
context . apiSuccess ( MODULE , 'Admin 查询本轮订单履约任务' , taskResponse ,
'审批 Outbox 收敛后应生成可查询的履约任务' , CONTRACT_OPTIONS )
2026-08-08 10:11:04 +08:00
context . assert ( MODULE , '履约任务与本轮订单和客户一致' , taskResponse ,
id ( task ? . orderId ) === orderId && id ( task ? . tenantId ) === tenantId ,
`taskId= ${ id ( task ? . id ) || 'missing' } , status= ${ task ? . status ?? 'missing' } ` , CONTRACT_OPTIONS )
if ( task ? . id ) {
context . createdResources . orders . taskId = id ( task . id )
const historyResponse = await context . request ( 'GET' ,
`/api/platform/v1/first-leg/tasks/ ${ encodeURIComponent ( task . id ) } /assignment-history?tenantId= ${ encodeURIComponent ( tenantId ) } &page=1&size=20` )
const history = context . apiSuccess ( MODULE , 'Admin 查询本轮履约任务分配历史' , historyResponse )
context . assert ( MODULE , '履约分配历史分页结构正确' , historyResponse , pageOk ( history ),
`taskId= ${ task . id } , total= ${ history ? . total ?? 'n/a' } ` )
}
2026-09-30 17:13:11 +08:00
await finishOrderLifecycle ( context , { tenantId , orderId , firstLegDetailPath })
2026-08-08 10:11:04 +08:00
}