2026-08-12 10:59:39 +08:00
const MODULE = 'finance-full-fixtures'
const OPTIONS = { scenario : 'finance-full-fixture-production' , deploymentBlocker : true }
function id ( value ) { return value == null ? '' : String ( value ) }
function key ( context , operation ) { return ` ${ context . marker } - ${ operation } ` . slice ( 0 , 128 ) }
function request ( context , method , path , body , token = context . token , extra = {}) {
return context . request ( method , path , body , token , { 'Idempotency-Key' : key ( context , path . replaceAll ( '/' , '_' )), ... extra })
}
function rows ( value ) { return Array . isArray ( value ) ? value : Array . isArray ( value ? . records ) ? value . records : [] }
function dateOffset ( days ) { const value = new Date (); value . setUTCDate ( value . getUTCDate () + days ); return value . toISOString (). slice ( 0 , 10 ) }
function blocker ( context , name , evidence ) { context . addStep ( MODULE , name , null , false , evidence , OPTIONS ) }
2026-08-15 17:45:51 +08:00
export function normalizeFinanceFixtureSources ( sources , orders , allocations , customerId , marker ) {
const orderById = new Map ( rows ( orders ). map (( order ) => [ id ( order ? . orderId ?? order ? . id ), order ]))
const allocationOrders = rows ( allocations ). map (( allocation ) => orderById . get ( id ( allocation ? . orderId ))). filter ( Boolean )
return rows ( sources ). map (( source ) => {
const sourceOrder = orderById . get ( id ( source ? . orderId ))
|| ( source ? . billingBasis === 'CONTAINER_SHARED' ? allocationOrders [ 0 ] : null )
return {
... source ,
customerId : id ( source ? . customerId ?? customerId ) || null ,
marker : source ? . marker ?? marker ,
orderId : id ( source ? . orderId ?? sourceOrder ? . orderId ?? sourceOrder ? . id ) || null ,
waybillId : id ( source ? . waybillId ?? sourceOrder ? . waybillId ) || null ,
shipmentId : id ( source ? . shipmentId ?? sourceOrder ? . shipmentId ) || null ,
orderNo : source ? . orderNo ?? sourceOrder ? . orderNo ?? null ,
}
})
}
2026-08-15 17:52:46 +08:00
export function supplierCategoryForFeeItem ( item ) {
2026-08-19 09:42:07 +08:00
return [ 'BOOKING' , 'CUSTOMS' , 'TRUCKING' , 'CERTIFICATE' ]. includes ( item ? . supplierCategory )
? item . supplierCategory : null
2026-08-15 17:52:46 +08:00
}
2026-08-15 17:58:15 +08:00
export function buildSupplierBody ( marker , category ) {
const suffix = String ( marker || '' ). replace ( /[^0-9A-Za-z]/g , '' ). slice ( - 24 )
return {
name : `财务验收供应商- ${ suffix } ` ,
category ,
2026-09-16 11:06:33 +08:00
truckingScope : category === 'TRUCKING' ? 'DOMESTIC' : null ,
2026-09-01 10:00:13 +08:00
...( category === 'BOOKING' ? { carrierCodes : [ 'MATSON' ] } : {}),
2026-08-15 17:58:15 +08:00
region : '宁波' , legalRepresentative : '财务验收法人' , contactName : '财务验收联系人' ,
phone : '13800000000' , email : `finance-supplier- ${ suffix || 'fixture' } @example.com` ,
address : '财务验收地址' , businessLicenseNo : `FIN ${ suffix } ` . slice ( 0 , 64 ),
licenseFileId : null , status : 'ACTIVE' , settlementPeriod : 'MONTHLY_30' , remark : marker ,
}
}
2026-08-12 12:35:29 +08:00
async function createOrder ( context , suffix , pricingProfile = null ) {
if ( context . financeFixtureHooks ? . createOrder ) return context . financeFixtureHooks . createOrder ( context , suffix , pricingProfile )
2026-08-12 10:59:39 +08:00
const originalMarker = context . marker
const original = context . createdResources . orders
2026-08-12 12:35:29 +08:00
const originalFetch = globalThis . fetch
2026-08-12 10:59:39 +08:00
context . marker = ` ${ originalMarker } - ${ suffix } `
2026-08-12 12:35:29 +08:00
if ( pricingProfile && typeof originalFetch === 'function' ) {
// The orders module discovers candidates independently. Reorder only those
// real responses so its payload uses one exact active price-book row.
globalThis . fetch = async ( input , init = {}) => {
const url = String ( input )
2026-09-09 17:42:26 +08:00
let effectiveInit = init
if (( init . method || 'GET' ) === 'POST'
&& url . includes ( '/api/merchant/v1/first-leg/pricing/estimate' )
&& typeof init . body === 'string' ) {
const body = JSON . parse ( init . body )
effectiveInit = {
... init ,
body : JSON . stringify ({ ... body , chargeMode : pricingProfile . chargeUnit })
}
}
const response = await originalFetch ( input , effectiveInit )
2026-08-12 12:35:29 +08:00
if (( init . method || 'GET' ) === 'GET' && ( url . includes ( '/api/merchant/v1/warehouses?' )
|| url . includes ( '/api/merchant/v1/first-leg/pricing/channels?' ))) {
const body = await response . clone (). json (). catch (() => null )
const data = body ? . code === '200' ? body . data : null
if ( url . includes ( 'warehouseType=FBA' )) {
const records = Array . isArray ( data ? . records ) ? data . records : []
const match = records . find (( item ) => id ( item . warehouseCode || item . code ) === id ( pricingProfile . destinationCode ))
if ( match ) body . data = { ... data , records : [ match , ... records . filter (( item ) => item !== match )] }
} else if ( url . includes ( 'warehouseType=DOMESTIC' )) {
const records = Array . isArray ( data ? . records ) ? data . records : []
const match = records . find (( item ) => id ( item . id ) === id ( pricingProfile . originWarehouseId ))
if ( match ) body . data = { ... data , records : [ match , ... records . filter (( item ) => item !== match )] }
} else {
const channels = Array . isArray ( data ) ? data : []
const match = channels . find (( item ) => id ( item . channelCode ) === id ( pricingProfile . channelCode ))
if ( match ) body . data = [{ ... match , pricingTaxModes : [ pricingProfile . pricingTaxMode ,
...( match . pricingTaxModes || []). filter (( mode ) => mode !== pricingProfile . pricingTaxMode )] },
... channels . filter (( item ) => item !== match )]
}
return new Response ( JSON . stringify ( body ), { status : response . status , statusText : response . statusText , headers : response . headers })
}
return response
}
}
2026-08-12 10:59:39 +08:00
try {
const orders = await import ( new URL ( './orders.mjs' , import . meta . url ))
await orders . run ( context )
const created = context . createdResources . orders
2026-08-12 12:35:29 +08:00
return created ? . marker === context . marker && created ? . orderId && created ? . inboundId
? { ... created , pricingProfile } : null
2026-08-12 10:59:39 +08:00
} finally {
2026-08-12 12:35:29 +08:00
globalThis . fetch = originalFetch
2026-08-12 10:59:39 +08:00
context . marker = originalMarker
if ( original ) context . createdResources . orders = original
else delete context . createdResources . orders
}
}
async function measureOrder ( context , resource ) {
if ( context . financeFixtureHooks ? . measureOrder ) return context . financeFixtureHooks . measureOrder ( context , resource )
const originalMarker = context . marker
const createdKeys = [ 'orders' , 'inboundMeasurement' , 'ocrInbound' , 'testInbound' ]
2026-09-09 17:42:26 +08:00
const contextKeys = [ 'inboundWeightOcr' , 'ocrInbound' , 'testInbound' , 'createdInbound' , 'allowAnyExistingPallet' , 'waitSplitVariant' ]
2026-08-12 10:59:39 +08:00
const createdSnapshot = new Map ( createdKeys . map (( key ) => [ key , { present : Object . hasOwn ( context . createdResources , key ), value : context . createdResources [ key ] }]))
const contextSnapshot = new Map ( contextKeys . map (( key ) => [ key , { present : Object . hasOwn ( context . context , key ), value : context . context [ key ] }]))
context . marker = resource . marker
context . createdResources . orders = resource
context . createdResources . ocrInbound = resource
context . createdResources . testInbound = resource
context . context . inboundWeightOcr = resource
context . context . ocrInbound = resource
context . context . testInbound = resource
context . context . createdInbound = resource
2026-09-01 10:00:13 +08:00
// Finance pricing profiles can point at any configured domestic warehouse.
// Reuse that warehouse's real pallet master instead of requiring the bulk-data
// generator's two fixed Yiwu pallet codes.
context . context . allowAnyExistingPallet = true
2026-09-09 17:42:26 +08:00
context . context . waitSplitVariant = {
...( context . context . waitSplitVariant || {}),
// The production pricing service chooses the dominant real billing basis.
// Keep the CBM sample's measured gross weight below its measured volume
// weight so the fixture proves the actual VOLUME_CBM path.
grossWeightPerPalletKg : resource . pricingProfile ? . chargeUnit === 'CBM' ? 20 : 100 ,
}
2026-08-12 10:59:39 +08:00
try {
const measurement = await import ( new URL ( './inbound-measurement.mjs' , import . meta . url ))
await measurement . run ( context )
const result = context . createdResources . inboundMeasurement
const detailResponse = await context . request ( 'GET' , `/api/platform/inbounds/ ${ encodeURIComponent ( resource . inboundId ) } ?tenantId= ${ encodeURIComponent ( resource . tenantId ) } ` )
const detail = context . apiSuccess ( MODULE , '回读审核测量实重与总体积' , detailResponse , `inboundId= ${ resource . inboundId } ` , OPTIONS )
const summary = detail ? . summary || {}
const actualWeightKg = Number ( summary . actualGrossWeightKg ?? summary . actualWeightKg )
const totalVolumeCbm = Number ( summary . actualVolumeCbm ?? summary . totalVolumeCbm )
context . assert ( MODULE , '审核测量快照实重和总体积均大于零' , detailResponse ,
Number . isFinite ( actualWeightKg ) && actualWeightKg > 0 && Number . isFinite ( totalVolumeCbm ) && totalVolumeCbm > 0 ,
`actualWeightKg= ${ actualWeightKg || 'missing' } , totalVolumeCbm= ${ totalVolumeCbm || 'missing' } ` , OPTIONS )
return result ? . marker === resource . marker ? { ... resource , ... result , actualWeightKg , totalVolumeCbm } : null
} finally {
context . marker = originalMarker
for ( const [ key , snapshot ] of createdSnapshot ) {
if ( snapshot . present ) context . createdResources [ key ] = snapshot . value
else delete context . createdResources [ key ]
}
for ( const [ key , snapshot ] of contextSnapshot ) {
if ( snapshot . present ) context . context [ key ] = snapshot . value
else delete context . context [ key ]
}
}
}
async function loadFeeItem ( context , direction ) {
if ( context . financeFixtureHooks ? . loadFeeItem ) return context . financeFixtureHooks . loadFeeItem ( context , direction )
const response = await context . request ( 'GET' , '/api/platform/v1/fee-items?page=1&pageSize=200' )
const list = context . apiSuccess ( MODULE , `查询 ${ direction } 人工费用项目` , response , '' , OPTIONS )
return rows ( list ). find (( item ) => item ? . status === 'ACTIVE' && ( direction === 'RECEIVABLE'
2026-08-15 11:05:58 +08:00
? item . warehouseReceivable === true
2026-08-19 09:42:07 +08:00
: item . warehousePayable === true )) || null
2026-08-12 10:59:39 +08:00
}
2026-08-19 09:42:07 +08:00
async function createManualFee ( context , resource , feeItem , direction , amount , basis , businessDate , supplier = null ) {
2026-08-12 10:59:39 +08:00
if ( context . financeFixtureHooks ? . createManualFee ) return context . financeFixtureHooks . createManualFee ( context , resource , feeItem , direction , amount , basis , businessDate )
2026-08-12 12:18:59 +08:00
const basePath = `/api/platform/v1/inbounds/ ${ encodeURIComponent ( resource . inboundId ) } /fee-details`
const listPath = ` ${ basePath } ?accountId= ${ encodeURIComponent ( resource . tenantId ) } `
2026-08-15 18:07:23 +08:00
const body = { feeItemId : String ( feeItem . id ), direction , businessDate ,
2026-08-19 09:42:07 +08:00
unitPrice : amount , currency : 'CNY' , amount ,
2026-08-24 11:48:16 +08:00
...( direction === 'PAYABLE' ? { settlementMode : 'ON_ACCOUNT' } : {}),
2026-08-19 09:42:07 +08:00
supplierId : supplier ? . id ? String ( supplier . id ) : null ,
2026-08-12 15:33:32 +08:00
remark : ` ${ context . marker } - ${ basis } ` }
2026-08-12 12:18:59 +08:00
const response = await request ( context , 'POST' , listPath , body )
2026-08-12 10:59:39 +08:00
const detail = context . apiSuccess ( MODULE , ` ${ basis } 人工费用明细` , response , `inboundId= ${ resource . inboundId } ` , OPTIONS )
const feeId = id ( detail ? . id )
context . assert ( MODULE , ` ${ basis } 费用来源身份和金额` , response ,
Boolean ( feeId ) && id ( detail ? . inboundId ) === id ( resource . inboundId ) && Number ( detail ? . amount ) === Number ( amount ),
`feeId= ${ feeId || 'missing' } , amount= ${ detail ? . amount ?? 'missing' } ` , OPTIONS )
if ( ! feeId ) return null
2026-08-12 12:18:59 +08:00
const approve = await request ( context , 'POST' , ` ${ basePath } / ${ feeId } /approve?accountId= ${ encodeURIComponent ( resource . tenantId ) } ` )
2026-08-12 10:59:39 +08:00
context . apiSuccess ( MODULE , ` ${ basis } 费用审核` , approve , '' , OPTIONS )
return { feeSourceId : feeId , sourceType : detail ? . sourceType || 'MANUAL' , direction , amount : Number ( amount ), businessDate ,
currency : 'CNY' , billingBasis : basis , inboundId : id ( resource . inboundId ), orderId : id ( resource . orderId ),
chargeableWeightKg : detail ? . chargeableWeightKg ?? null , chargeableVolumeCbm : detail ? . chargeableVolumeCbm ?? null }
}
2026-08-13 20:00:30 +08:00
async function loadPublishedAutomaticFee ( context , resource ) {
if ( context . financeFixtureHooks ? . loadPublishedAutomaticFee ) return context . financeFixtureHooks . loadPublishedAutomaticFee ( context , resource )
2026-08-12 10:59:39 +08:00
const pricingInboundResponse = await context . request ( 'GET' ,
`/api/platform/inbounds/ ${ encodeURIComponent ( resource . inboundId ) } ?tenantId= ${ encodeURIComponent ( resource . tenantId ) } ` )
2026-08-13 20:00:30 +08:00
const pricingInbound = context . apiSuccess ( MODULE , '回读审核完成后的入库主表状态' , pricingInboundResponse ,
2026-08-12 10:59:39 +08:00
`inboundId= ${ resource . inboundId } ` , OPTIONS )
const pricingSummary = pricingInbound ? . summary || {}
const actualWeightKg = Number ( pricingSummary . actualGrossWeightKg ?? pricingSummary . actualWeightKg )
const actualVolumeCbm = Number ( pricingSummary . actualVolumeCbm ?? pricingSummary . totalVolumeCbm )
if ( ! ( actualWeightKg > 0 ) || ! ( actualVolumeCbm > 0 )) {
blocker ( context , '自动计费前审核测量快照完整' ,
`服务端计费实际读取 fl_inbound_order.actual_gross_weight_kg/actual_volume_cbm; inboundId= ${ resource . inboundId } , actualWeightKg= ${ actualWeightKg || 'missing' } , actualVolumeCbm= ${ actualVolumeCbm || 'missing' } ;测量表有值但主表未同步` )
return null
}
2026-08-14 23:03:36 +08:00
const pricingBase = `/api/platform/v1/inbounds/ ${ encodeURIComponent ( resource . inboundId ) } /fees`
const tenantQuery = `tenantId= ${ encodeURIComponent ( resource . tenantId ) } `
const latestResponse = await context . request ( 'GET' , ` ${ pricingBase } /latest? ${ tenantQuery } ` )
let latest = context . apiSuccess ( MODULE , '回读测量审核生成的自动费用试算' , latestResponse ,
`inboundId= ${ resource . inboundId } ` , OPTIONS )
if ( ! latest ? . feeId || ! [ 'CALCULATED' , 'CONFIRMED' , 'SENT' ]. includes ( latest ? . feeStatus )) {
blocker ( context , '测量审核已生成可确认的自动费用试算' ,
`inboundId= ${ resource . inboundId } , feeId= ${ id ( latest ? . feeId ) || 'missing' } , feeStatus= ${ latest ? . feeStatus || 'missing' } , requestId= ${ latestResponse ? . requestId || 'missing' } , traceId= ${ latestResponse ? . traceId || 'missing' } ;审核只负责初算,Finance fixture 不重复试算` )
return null
}
if ( latest . feeStatus === 'CALCULATED' ) {
const confirmResponse = await request ( context , 'POST' , ` ${ pricingBase } /confirm? ${ tenantQuery } ` )
latest = context . apiSuccess ( MODULE , '确认测量审核生成的自动费用试算' , confirmResponse ,
`inboundId= ${ resource . inboundId } , feeId= ${ id ( latest . feeId ) } ` , OPTIONS )
}
if ( latest ? . feeStatus === 'CONFIRMED' ) {
const sendResponse = await request ( context , 'POST' , ` ${ pricingBase } /send? ${ tenantQuery } ` )
latest = context . apiSuccess ( MODULE , '发送已确认自动费用并发布来源' , sendResponse ,
`inboundId= ${ resource . inboundId } , feeId= ${ id ( latest . feeId ) } ` , OPTIONS )
}
if ( latest ? . feeStatus !== 'SENT' ) {
blocker ( context , '自动费用确认发送状态闭环' ,
`inboundId= ${ resource . inboundId } , feeId= ${ id ( latest ? . feeId ) || 'missing' } , feeStatus= ${ latest ? . feeStatus || 'missing' } ` )
return null
}
2026-08-13 20:00:30 +08:00
const listPath = `/api/platform/v1/inbounds/ ${ encodeURIComponent ( resource . inboundId ) } /fee-details?accountId= ${ encodeURIComponent ( resource . tenantId ) } `
const listResponse = await context . request ( 'GET' , listPath )
const details = context . apiSuccess ( MODULE , '回读审核链已发布自动费用来源' , listResponse ,
`inboundId= ${ resource . inboundId } ` , OPTIONS )
const published = rows ( details ). find (( item ) => item ? . sourceType === 'FBA_PRICING'
2026-08-19 09:42:07 +08:00
&& item ? . reviewStatus === 'APPROVED' && item ? . publicationStatus === 'PUBLISHED' )
2026-08-13 20:00:30 +08:00
if ( ! published ) {
blocker ( context , '审核链已生成可结算自动费用来源' ,
2026-08-14 23:03:36 +08:00
`inboundId= ${ resource . inboundId } , processStatus= ${ pricingSummary . processStatus || pricingInbound ? . processStatus || 'missing' } , feeId= ${ id ( latest . feeId ) } , feeStatus= ${ latest . feeStatus } , feeDetails= ${ rows ( details ). length } ;确认和发送成功后应同步生成 APPROVED + PUBLISHED 的 FBA_PRICING 来源` )
2026-08-13 20:00:30 +08:00
return null
}
const feeSourceId = id ( published . sourceFeeId )
const billingBasis = published . chargeUnit === 'KG' ? 'WEIGHT_KG'
: published . chargeUnit === 'CBM' ? 'VOLUME_CBM' : null
const responseWeightKg = Number ( published . actualGrossWeightKg ?? actualWeightKg )
const responseVolumeCbm = Number ( published . actualVolumeCbm ?? actualVolumeCbm )
context . assert ( MODULE , '已发布自动费用来源、金额和计费依据完整' , listResponse ,
Boolean ( feeSourceId ) && Boolean ( billingBasis ) && Number ( published . amount ) >= 0
2026-08-12 10:59:39 +08:00
&& responseWeightKg > 0 && responseVolumeCbm > 0 ,
2026-08-13 20:00:30 +08:00
`feeSourceId= ${ feeSourceId || 'missing' } , detailId= ${ id ( published . id ) || 'missing' } , chargeUnit= ${ published . chargeUnit || 'missing' } , amount= ${ published . amount ?? 'missing' } , actualWeightKg= ${ responseWeightKg || 'missing' } , actualVolumeCbm= ${ responseVolumeCbm || 'missing' } ` , OPTIONS )
2026-08-12 10:59:39 +08:00
if ( ! feeSourceId || ! billingBasis ) return null
return { feeSourceId , orderId : id ( resource . orderId ), inboundId : id ( resource . inboundId ), sourceType : 'FBA_PRICING' ,
2026-08-13 20:00:30 +08:00
direction : 'RECEIVABLE' , currency : published . currency ,
amount : Number ( published . amount ), businessDate : String ( published . businessDate || published . publishedAt || '' ). slice ( 0 , 10 ),
billingBasis , chargeableQuantity : published . chargeableQuantity ,
chargeUnit : published . chargeUnit ,
2026-08-12 10:59:39 +08:00
actualWeightKg : responseWeightKg , actualVolumeCbm : responseVolumeCbm }
}
2026-08-15 17:52:46 +08:00
async function loadSupplier ( context , category ) {
if ( context . financeFixtureHooks ? . loadSupplier ) return context . financeFixtureHooks . loadSupplier ( context , category )
2026-08-19 09:42:07 +08:00
const effectiveCategory = category || 'BOOKING'
const body = buildSupplierBody ( context . marker , effectiveCategory )
2026-08-15 17:58:15 +08:00
const response = await request ( context , 'POST' , '/api/platform/v1/suppliers?tenantId=0' , body )
const supplierId = id ( context . apiSuccess ( MODULE , '创建本轮 Finance 隔离供应商' , response ,
2026-08-19 09:42:07 +08:00
`category= ${ effectiveCategory } ` , OPTIONS ))
2026-08-15 17:58:15 +08:00
if ( ! supplierId ) return null
const detailResponse = await context . request ( 'GET' , `/api/platform/v1/suppliers/ ${ encodeURIComponent ( supplierId ) } ?tenantId=0` )
const detail = context . apiSuccess ( MODULE , '回读本轮 Finance 隔离供应商' , detailResponse ,
`supplierId= ${ supplierId } ` , OPTIONS )
context . assert ( MODULE , 'Finance 供应商为 ACTIVE 且类别匹配费用项目' , detailResponse ,
2026-08-19 09:42:07 +08:00
id ( detail ? . id ) === supplierId && detail ? . status === 'ACTIVE' && ( ! category || detail ? . category === category )
2026-08-15 17:58:15 +08:00
&& detail ? . name === body . name ,
`supplierId= ${ supplierId } , status= ${ detail ? . status || 'missing' } , category= ${ detail ? . category || 'missing' } ` , OPTIONS )
2026-08-19 09:42:07 +08:00
return detail ? . status === 'ACTIVE' && ( ! category || detail ? . category === category ) ? detail : null
2026-08-12 10:59:39 +08:00
}
async function uploadVoucher ( context , customerId ) {
if ( context . financeFixtureHooks ? . uploadVoucher ) return context . financeFixtureHooks . uploadVoucher ( context , customerId )
const path = `/api/platform/finance/v1/vouchers?customerId= ${ encodeURIComponent ( customerId ) } `
const form = new FormData ()
const png = Buffer . from ( 'iVBORw0KGgoAAAANSUhEUgAAAAEAAAABCAQAAAC1HAwCAAAAC0lEQVR42mNk+A8AAQUBAScY42YAAAAASUVORK5CYII=' , 'base64' )
form . append ( 'file' , new Blob ([ png ], { type : 'image/png' }), `finance- ${ context . marker } .png` )
try {
const response = await fetch ( ` ${ context . options . gateway }${ path } ` , { method : 'POST' ,
headers : { Accept : 'application/json' , Authorization : `Bearer ${ context . token } ` }, body : form ,
signal : AbortSignal . timeout ( 30_000 ) })
const body = await response . json (). catch (() => null )
const evidence = { method : 'POST' , path , http : response . status , body , raw : null ,
requestId : body ? . requestId || response . headers . get ( 'x-request-id' ), traceId : body ? . traceId || response . headers . get ( 'x-trace-id' ) }
const data = context . apiSuccess ( MODULE , '上传本轮 Finance 凭证' , evidence , '' , OPTIONS )
return id ( data ? . fileId ) || null
} catch ( error ) {
blocker ( context , '上传本轮 Finance 凭证' , `multipart request failed: ${ String ( error ) } ` )
return null
}
}
async function loadPricingEvidence ( context ) {
if ( context . financeFixtureHooks ? . loadPricingEvidence ) return context . financeFixtureHooks . loadPricingEvidence ( context )
2026-09-09 18:33:39 +08:00
const fbaInternalCodes = new Set ()
let warehousePage = 1
let warehouseTotal = 0
do {
const warehouseResponse = await context . request ( 'GET' ,
`/api/platform/v1/warehouse-admin?tenantId=0&page= ${ warehousePage } &size=200&warehouseType=FBA&status=ACTIVE` )
const warehouseResult = context . apiSuccess ( MODULE , `读取具备内部编号的 FBA 仓第 ${ warehousePage } 页` ,
warehouseResponse , '' , OPTIONS )
const warehouses = rows ( warehouseResult )
warehouseTotal = Number ( warehouseResult ? . total || warehouses . length )
warehouses . filter (( item ) => item ? . internalCode && item ? . warehouseCode )
. forEach (( item ) => fbaInternalCodes . add ( String ( item . warehouseCode ). trim (). toUpperCase ()))
if ( warehouses . length === 0 ) break
warehousePage += 1
} while (( warehousePage - 1 ) * 200 < warehouseTotal )
context . assert ( MODULE , '自动计费候选使用已配置内部编号的 FBA 仓' , null ,
fbaInternalCodes . size > 0 , `warehouses= ${ fbaInternalCodes . size } ` , OPTIONS )
2026-08-12 10:59:39 +08:00
const response = await context . request ( 'GET' , '/api/platform/v1/fba-pricing/carrier-rates/price-books?status=ACTIVE' )
const versions = context . apiSuccess ( MODULE , '读取真实生效价格配置' , response , '' , OPTIONS )
2026-08-12 12:35:29 +08:00
const active = rows ( versions ). filter (( item ) => item ? . status === 'ACTIVE' )
const profiles = []
for ( const version of active ) {
const detailResponse = await context . request ( 'GET' , `/api/platform/v1/fba-pricing/carrier-rates/price-books/ ${ encodeURIComponent ( version . id ) } ` )
const detail = context . apiSuccess ( MODULE , '读取 ACTIVE 价格簿真实费率行' , detailResponse , `versionId= ${ version . id } ` , OPTIONS )
const rates = Array . isArray ( detail ? . rates ) ? detail . rates : []
for ( const rate of rates ) {
if ( rate ? . destinationType !== 'FBA' || ! rate ? . originWarehouseId || ! rate ? . channelCode || ! rate ? . pricingTaxMode || ! rate ? . destinationCode ) continue
2026-09-09 18:33:39 +08:00
if ( ! fbaInternalCodes . has ( String ( rate . destinationCode ). trim (). toUpperCase ())) continue
2026-08-12 12:35:29 +08:00
if ( ! [ 'KG' , 'CBM' ]. includes ( rate . chargeUnit ) || profiles . some (( item ) => item . chargeUnit === rate . chargeUnit )) continue
const matches = rates . filter (( item ) => id ( item . originWarehouseId ) === id ( rate . originWarehouseId )
&& item . channelCode === rate . channelCode && item . destinationType === rate . destinationType
&& String ( item . destinationCode ). toUpperCase () === String ( rate . destinationCode ). toUpperCase ()
&& item . pricingTaxMode === rate . pricingTaxMode )
if ( matches . length !== 1 ) continue
profiles . push ({ versionId : id ( version . id ), rateId : id ( rate . id ), chargeUnit : rate . chargeUnit ,
originWarehouseId : id ( rate . originWarehouseId ), channelCode : rate . channelCode ,
2026-09-09 17:42:26 +08:00
pricingTaxMode : rate . pricingTaxMode , destinationCode : rate . destinationCode ,
businessType : rate . businessType || 'FBA' })
2026-08-12 12:35:29 +08:00
}
}
context . assert ( MODULE , 'ACTIVE 价格簿包含 KG 与 CBM 可用真实组合' , response ,
profiles . some (( item ) => item . chargeUnit === 'KG' ) && profiles . some (( item ) => item . chargeUnit === 'CBM' ),
`units= ${ profiles . map (( item ) => item . chargeUnit ). join ( ',' ) || 'none' } ` , OPTIONS )
return { activeVersionIds : active . map (( item ) => id ( item . id )). filter ( Boolean ), profiles }
}
2026-09-09 17:42:26 +08:00
async function ensureCustomerChargeRule ( context , resource , profile ) {
if ( context . financeFixtureHooks ? . ensureCustomerChargeRule ) {
return context . financeFixtureHooks . ensureCustomerChargeRule ( context , resource , profile )
}
if ( ! resource ? . tenantId || profile ? . pricingTaxMode !== 'TAX_EXCLUDED' ) return null
const tenantId = id ( resource . tenantId )
const inboundResponse = await context . request ( 'GET' ,
`/api/platform/inbounds/ ${ encodeURIComponent ( resource . inboundId ) } ?tenantId= ${ encodeURIComponent ( tenantId ) } ` )
const inbound = context . apiSuccess ( MODULE , '读取不包税入库计费业务类型' , inboundResponse ,
`inboundId= ${ resource . inboundId } ` , OPTIONS )
const businessType = inbound ? . summary ? . businessType || profile . businessType
if ( ! businessType ) {
blocker ( context , '取得不包税入库计费业务类型' ,
`inboundId= ${ resource . inboundId } 未返回 businessType,不能创建猜测的客户计费规则` )
return null
}
const listPath = `/api/platform/v1/fba-pricing/customer-rules?tenantId= ${ encodeURIComponent ( tenantId ) } &customerId= ${ encodeURIComponent ( tenantId ) } `
const listResponse = await context . request ( 'GET' , listPath )
const listed = context . apiSuccess ( MODULE , '查询不包税客户计费规则' , listResponse ,
`customerId= ${ tenantId } , businessType= ${ businessType } ` , OPTIONS )
const applicable = rows ( listed ). filter (( rule ) => rule ? . status === 'ACTIVE'
&& rule ? . businessType === businessType && rule ? . taxMode === 'TAX_EXCLUDED' )
let desired = applicable . find (( rule ) => rule ? . chargeMode === profile . chargeUnit )
if ( ! desired ) {
const effectiveFrom = new Date ( Date . now () - 86_400_000 ). toISOString (). slice ( 0 , 19 )
const body = {
ruleCode : `FIN_P1_ ${ profile . chargeUnit } _ ${ tenantId } ` . slice ( 0 , 50 ), customerId : tenantId ,
2026-09-09 17:46:36 +08:00
customerName : String ( inbound ? . summary ? . customerName || `财务P1验收客户- ${ tenantId } ` ). slice ( 0 , 128 ), businessType ,
2026-09-09 17:42:26 +08:00
chargeMode : profile . chargeUnit , taxMode : 'TAX_EXCLUDED' , conversionFactor : 1 ,
roundingScale : 3 , roundingMode : 'HALF_UP' , currency : 'CNY' , effectiveFrom ,
effectiveTo : null , remark : ` ${ context . marker } -VOLUME_CBM`
}
const createResponse = await request ( context , 'POST' ,
`/api/platform/v1/fba-pricing/customer-rules/versions?tenantId= ${ encodeURIComponent ( tenantId ) } ` , body )
desired = context . apiSuccess ( MODULE , '创建不包税客户计费规则' , createResponse ,
`customerId= ${ tenantId } , chargeMode= ${ profile . chargeUnit } ` , OPTIONS )
}
const needsDefault = applicable . length > 1
|| ( applicable . length === 1 && id ( applicable [ 0 ] ? . id ) !== id ( desired ? . id ))
if ( desired ? . id && needsDefault && desired . defaultRule !== true ) {
const defaultResponse = await request ( context , 'PUT' ,
`/api/platform/v1/fba-pricing/customer-rules/ ${ encodeURIComponent ( desired . id ) } /default?tenantId= ${ encodeURIComponent ( tenantId ) } ` )
context . apiSuccess ( MODULE , '设置不包税客户唯一默认计费规则' , defaultResponse ,
`ruleId= ${ desired . id } , chargeMode= ${ profile . chargeUnit } ` , OPTIONS )
}
context . assert ( MODULE , '不包税自动计费具备客户计费方式' , listResponse ,
Boolean ( desired ? . id ) && desired ? . chargeMode === profile . chargeUnit ,
`customerId= ${ tenantId } , chargeMode= ${ desired ? . chargeMode || 'missing' } , ruleId= ${ id ( desired ? . id ) || 'missing' } ` , OPTIONS )
return desired || null
}
2026-08-12 12:35:29 +08:00
async function createMarkerContainer ( context , resource ) {
if ( context . financeFixtureHooks ? . createMarkerContainer ) return context . financeFixtureHooks . createMarkerContainer ( context , resource )
2026-08-13 18:34:44 +08:00
if ( ! resource ) {
blocker ( context , '取得可创建柜次的测量入库单' ,
'本轮没有完成测量的 marker-owned 入库单;跳过柜次及整柜成本 fixture,等待订单/测量前置修复' )
return null
}
2026-08-12 12:35:29 +08:00
if ( context . financeFixtureHooks ) return null
const original = context . createdResources . phase3ContainerLoading
const originalMarker = context . marker
const snapshots = [ 'orders' , 'inboundMeasurement' ]. map (( key ) => [ key ,
{ present : Object . hasOwn ( context . createdResources , key ), value : context . createdResources [ key ] }])
context . createdResources . orders = resource
context . createdResources . inboundMeasurement = resource
context . marker = resource . marker || originalMarker
try {
const module = await import ( new URL ( './phase3-container-loading.mjs' , import . meta . url ))
await module . run ( context )
const created = context . createdResources . phase3ContainerLoading
return created ? . marker === context . marker && created ? . containerBookingId && created ? . loadingPlanId ? { ... created } : null
} finally {
context . marker = originalMarker
for ( const [ key , snapshot ] of snapshots ) {
if ( snapshot . present ) context . createdResources [ key ] = snapshot . value
else delete context . createdResources [ key ]
}
if ( original ) context . createdResources . phase3ContainerLoading = original
else delete context . createdResources . phase3ContainerLoading
}
2026-08-12 10:59:39 +08:00
}
2026-08-14 10:09:46 +08:00
export function buildContainerCostBody ( marker , customerId , container , voucherFileId ) {
return { customerId : String ( customerId ), costType : 'CONTAINER_SHARED' , orderId : null ,
2026-08-12 13:24:17 +08:00
// Finance names this field containerId, but its Firstleg cost snapshot contract
// resolves it as the loading plan ID, not the physical container booking ID.
2026-08-14 10:09:46 +08:00
containerId : String ( container . loadingPlanId ), expenseName : ` ${ marker } -整柜成本` , amount : 300 ,
currency : 'CNY' , voucherFileIds : [ voucherFileId ], remark : marker }
}
async function createContainerCost ( context , customerId , container , voucherFileId , snapshotInput ) {
if ( context . financeFixtureHooks ? . createContainerCost ) return context . financeFixtureHooks . createContainerCost ( context , customerId , container , voucherFileId , snapshotInput )
const body = buildContainerCostBody ( context . marker , customerId , container , voucherFileId )
2026-08-12 10:59:39 +08:00
const response = await request ( context , 'POST' , '/api/platform/finance/v1/cost-entries' , body )
2026-08-12 13:24:17 +08:00
if ( response ? . body ? . code === 'FIN_400' ) {
const first = snapshotInput ? . items ? .[ 0 ]
blocker ( context , 'Finance 整柜成本请求关联真实装柜计划' ,
`message= ${ response . body . message || 'missing' } , loadingPlanId= ${ container . loadingPlanId || 'missing' } , containerBookingId= ${ container . containerBookingId || 'missing' } , voucherFileId= ${ voucherFileId || 'missing' } , publicSnapshotValid= ${ snapshotInput ? . valid === true } , validItems= ${ snapshotInput ? . items ? . length || 0 } , accountId= ${ first ? . accountId ?? 'missing' } , orderId= ${ first ? . orderId ?? 'missing' } , volumeCbm= ${ first ? . volumeCbm ?? 'missing' } , requestId= ${ response . requestId || 'missing' } , traceId= ${ response . traceId || 'missing' } ;若 publicSnapshotValid=true,说明 fixture ID 与公开装柜明细有效,阻断位于 Finance -> Firstleg 内部快照服务部署/契约` )
}
2026-08-12 10:59:39 +08:00
const cost = context . apiSuccess ( MODULE , '创建整柜成本并触发真实体积分摊' , response , '' , OPTIONS )
2026-08-12 14:40:05 +08:00
if ( ! cost ? . id ) return null
const detailResponse = await context . request ( 'GET' ,
`/api/platform/finance/v1/cost-entries/ ${ encodeURIComponent ( cost . id ) } ?customerId= ${ encodeURIComponent ( customerId ) } ` )
const detail = context . apiSuccess ( MODULE , '回读整柜成本分摊结果' , detailResponse ,
`costId= ${ cost . id } ` , OPTIONS )
return { costId : id ( cost . id ), allocations : Array . isArray ( detail ? . allocations ) ? detail . allocations : null }
2026-08-12 10:59:39 +08:00
}
2026-08-12 13:24:17 +08:00
async function verifyContainerCostSnapshotInput ( context , customerId , container ) {
if ( context . financeFixtureHooks ? . verifyContainerCostSnapshotInput ) {
return context . financeFixtureHooks . verifyContainerCostSnapshotInput ( context , customerId , container )
}
const path = `/api/platform/v1/first-leg/smart-loading/plans/ ${ encodeURIComponent ( container . loadingPlanId ) } `
const response = await context . request ( 'GET' , path )
const plan = context . apiSuccess ( MODULE , '回读 Finance 柜次体积快照输入' , response ,
`loadingPlanId= ${ container . loadingPlanId } ` , OPTIONS )
const items = Array . isArray ( plan ? . items ) ? plan . items : []
const valid = items . filter (( item ) => id ( item ? . accountId ) === id ( customerId )
&& Number ( item ? . orderId ) > 0 && Number ( item ? . volumeCbm ) > 0 )
context . assert ( MODULE , '装柜明细满足 Finance 内部快照筛选条件' , response ,
plan ? . status === 'COMPLETED' && valid . length > 0 ,
`status= ${ plan ? . status || 'missing' } , items= ${ items . length } , validItems= ${ valid . length } , accountId= ${ items [ 0 ] ? . accountId ?? 'missing' } , orderId= ${ items [ 0 ] ? . orderId ?? 'missing' } , volumeCbm= ${ items [ 0 ] ? . volumeCbm ?? 'missing' } ` , OPTIONS )
return { valid : plan ? . status === 'COMPLETED' && valid . length > 0 , items : valid }
}
2026-08-12 10:59:39 +08:00
export async function run ( context ) {
const start = context . steps . length
const pricingEvidence = await loadPricingEvidence ( context )
const resources = []
2026-08-12 12:35:29 +08:00
const profiles = pricingEvidence . profiles || []
2026-09-09 17:42:26 +08:00
for ( const [ index , suffix ] of [ 'AUTO1' , 'AUTO2' , 'AUTO3' , 'AUTO4' , 'MANUAL' , 'CONTAINER' ]. entries ()) {
2026-08-12 12:35:29 +08:00
const resource = await createOrder ( context , suffix , profiles [ index % profiles . length ] || null )
2026-08-12 10:59:39 +08:00
context . assert ( MODULE , `创建 ${ suffix } marker 隔离订单与入库单` , null , Boolean ( resource ),
resource ? `orderId= ${ resource . orderId } , inboundId= ${ resource . inboundId } ` : `marker= ${ context . marker } - ${ suffix } 未返回独立入库单` , OPTIONS )
if ( resource ) resources . push ( resource )
}
2026-09-09 17:42:26 +08:00
for ( const resource of resources . filter (( item ) => item . pricingProfile ? . pricingTaxMode === 'TAX_EXCLUDED' )) {
await ensureCustomerChargeRule ( context , resource , resource . pricingProfile )
}
2026-08-12 10:59:39 +08:00
const measured = []
for ( const resource of resources ) {
const result = await measureOrder ( context , resource )
if ( result ) measured . push ({ ... resource , ... result })
}
const feeItems = {
RECEIVABLE : await loadFeeItem ( context , 'RECEIVABLE' ),
PAYABLE : await loadFeeItem ( context , 'PAYABLE' ),
}
2026-08-15 17:52:46 +08:00
const payableSupplierCategory = supplierCategoryForFeeItem ( feeItems . PAYABLE )
const supplier = await loadSupplier ( context , payableSupplierCategory )
if ( ! supplier ? . id ) blocker ( context , '取得 Warehouse PAYABLE 供应商' ,
2026-08-19 09:42:07 +08:00
`未找到 ACTIVE ${ payableSupplierCategory || '任意类别' } 供应商;不能伪造 supplierId` )
2026-08-12 10:59:39 +08:00
for ( const direction of [ 'RECEIVABLE' , 'PAYABLE' ]) {
if ( ! feeItems [ direction ]) context . addCoverageGap ( MODULE , `missing- ${ direction . toLowerCase () } -fee-item` , ` ${ direction } 人工费用 fixture` ,
`没有找到带 warehouse ${ direction === 'RECEIVABLE' ? 'Receivable' : 'Payable' } =true 的 ACTIVE 费用项目,禁止猜测费用项目 ID` , 'missing-deployed-fee-item' )
}
const fixtures = []
for ( const resource of measured . slice ( 0 , 4 )) {
2026-08-13 20:00:30 +08:00
const fee = await loadPublishedAutomaticFee ( context , resource )
2026-08-12 10:59:39 +08:00
if ( fee ) fixtures . push ( fee )
}
for ( const basis of [ 'WEIGHT_KG' , 'VOLUME_CBM' ]) {
if ( ! fixtures . some (( fee ) => fee . billingBasis === basis )) {
blocker ( context , ` ${ basis } 自动计费 fixture` ,
`activePriceVersions= ${ pricingEvidence . activeVersionIds ? . join ( ',' ) || 'none' } ;本轮隔离订单未产生 ${ basis } 来源,价格配置/订单条件不支持,不能伪装成功` )
}
}
2026-08-12 12:18:59 +08:00
const receivableResources = measured . slice ( 0 , 4 )
if ( feeItems . RECEIVABLE ) {
for ( const [ index , resource ] of receivableResources . entries ()) {
const fee = await createManualFee ( context , resource , feeItems . RECEIVABLE , 'RECEIVABLE' , 100 + index * 10 ,
'MANUAL_RECEIVABLE' , dateOffset ( - 1 - ( index % 2 )))
if ( fee ) fixtures . push ( fee )
}
}
2026-09-09 17:42:26 +08:00
const manualResources = [ measured [ 4 ]]. filter ( Boolean )
2026-08-12 12:18:59 +08:00
if ( feeItems . PAYABLE && supplier ? . id ) {
for ( const [ index , manualResource ] of manualResources . entries ()) {
const fee = await createManualFee ( context , manualResource , feeItems . PAYABLE , 'PAYABLE' , 80 + index * 10 ,
2026-08-12 15:33:32 +08:00
'MANUAL_ORDER_FEE' , dateOffset ( - 3 - index ), supplier )
2026-08-19 09:42:07 +08:00
if ( fee ) fixtures . push ({ ... fee , supplierId : id ( supplier . id ),
2026-08-15 17:52:46 +08:00
supplierName : supplier . name || null , supplierCategory : supplier . category || payableSupplierCategory })
2026-08-12 12:18:59 +08:00
}
2026-08-12 10:59:39 +08:00
}
2026-09-09 17:42:26 +08:00
// A source whose fee has already been sent is intentionally frozen against
// loading-plan membership changes. Use a dedicated measured order for the
// container fixture so finance setup never conflicts with that invariant.
const containerResource = measured [ 5 ]
const container = ( containerResource ? await createMarkerContainer ( context , containerResource ) : null )
2026-08-12 12:35:29 +08:00
|| context . createdResources ? . phase3ContainerLoading
const containerOwned = ( container ? . marker === context . marker || String ( container ? . marker || '' ). startsWith ( ` ${ context . marker } -` ))
2026-08-12 10:59:39 +08:00
&& container ? . loadingPlanId && container ? . containerBookingId
let containerFixture = null
if ( containerOwned && feeItems . PAYABLE && supplier ? . id ) {
2026-08-12 12:18:59 +08:00
const basePath = `/api/platform/v1/loading-plans/ ${ encodeURIComponent ( container . loadingPlanId ) } /fee-details`
const path = ` ${ basePath } ?accountId= ${ encodeURIComponent ( container . tenantId ) } `
2026-08-15 18:07:23 +08:00
const body = { feeItemId : String ( feeItems . PAYABLE . id ), direction : 'PAYABLE' , businessDate : dateOffset ( - 2 ),
2026-08-24 11:48:16 +08:00
amount : 300 , currency : 'CNY' , supplierId : id ( supplier . id ), settlementMode : 'ON_ACCOUNT' ,
remark : ` ${ context . marker } -CONTAINER_SHARED` }
2026-08-12 10:59:39 +08:00
const response = await request ( context , 'POST' , path , body )
2026-08-12 12:47:06 +08:00
if ( response ? . http === 404 ) {
blocker ( context , '按柜费用来源接口已部署并可路由' ,
`POST ${ path } returned HTTP 404 before Controller handling; Firstleg Controller contract is /api/platform/v1/loading-plans/{loadingPlanId}/fee-details, but current Gateway route does not expose /api/platform/v1/loading-plans/**; requestId= ${ response . requestId || 'missing' } , traceId= ${ response . traceId || 'missing' } ` )
}
2026-08-12 10:59:39 +08:00
const detail = context . apiSuccess ( MODULE , '创建按柜费用来源' , response , '' , OPTIONS )
const feeId = id ( detail ? . id )
2026-08-12 12:18:59 +08:00
if ( feeId ) { const approval = await request ( context , 'POST' , ` ${ basePath } / ${ feeId } /approve?accountId= ${ encodeURIComponent ( container . tenantId ) } ` ); context . apiSuccess ( MODULE , '审核按柜费用来源' , approval , '' , OPTIONS ) }
2026-08-12 10:59:39 +08:00
containerFixture = { feeSourceId : feeId || null , sourceType : 'MANUAL' , direction : 'PAYABLE' , amount : 300 ,
businessDate : body . businessDate , currency : 'CNY' , billingBasis : 'CONTAINER_SHARED' ,
2026-08-19 09:42:07 +08:00
supplierId : supplier ? . id ? id ( supplier . id ) : null ,
2026-08-15 17:52:46 +08:00
supplierName : supplier ? . name || null , supplierCategory : supplier ? . category || payableSupplierCategory ,
2026-08-12 10:59:39 +08:00
containerBookingId : id ( container . containerBookingId ), containerNo : container . containerNo || null ,
loadingPlanId : id ( container . loadingPlanId ) }
2026-08-15 18:03:47 +08:00
} else if ( ! containerOwned ) {
2026-08-12 10:59:39 +08:00
context . addStep ( MODULE , '取得同 marker 柜次用于整柜费用分摊' , null , false ,
'没有同 marker 的已完成柜次和可核验 containerBookingId;不读取历史柜次、不猜测柜号' , OPTIONS )
context . addCoverageGap ( MODULE , 'container-shared-cost-allocation' , '按柜添加费用并按订单/运单体积分摊' ,
'当前上下文没有同 marker 的已完成柜次及真实 Firstleg 体积快照,禁止借用历史柜次或模拟 Finance 分摊成功' , 'no-marker-owned-container-snapshot' )
2026-08-15 18:03:47 +08:00
} else {
context . addCoverageGap ( MODULE , 'container-shared-cost-allocation' , '按柜添加费用并按订单/运单体积分摊' ,
2026-08-19 09:42:07 +08:00
'已识别同 marker 的 COMPLETED 柜次,但缺少 PAYABLE 费用项目或符合其显式可空类别约束的 ACTIVE 供应商' , 'missing-payable-supplier' )
2026-08-12 10:59:39 +08:00
}
if ( containerFixture ) fixtures . push ( containerFixture )
const customerId = id ( measured [ 0 ] ? . tenantId ) || null
const voucherFileId = customerId ? await uploadVoucher ( context , customerId ) : null
if ( ! voucherFileId ) blocker ( context , '取得本轮 Finance voucherFileId' ,
'未通过 /api/platform/finance/v1/vouchers multipart 上传取得真实 fileId;整柜成本不得模拟成功' )
let allocations = []
if ( containerFixture ? . feeSourceId && voucherFileId && supplier ? . id ) {
2026-08-12 13:24:17 +08:00
const snapshotInput = await verifyContainerCostSnapshotInput ( context , customerId , container )
const cost = snapshotInput . valid ? await createContainerCost ( context , customerId , container , voucherFileId , snapshotInput ) : null
2026-08-12 10:59:39 +08:00
if ( Array . isArray ( cost ? . allocations )) allocations = cost . allocations
else blocker ( context , '回读整柜成本分摊结果' ,
2026-08-12 14:40:05 +08:00
`costId= ${ cost ? . costId || 'missing' } ;Finance 成本详情未返回真实 fin_cost_allocation,禁止从金额和体积自行伪造持久化结果` )
2026-08-12 10:59:39 +08:00
}
if ( allocations . length === 0 ) blocker ( context , '输出真实订单/运单分摊' ,
'缺少已完成柜次、凭证、供应商或 Finance 分摊响应;不伪造 allocation ID/金额' )
const distinctBusinessDates = new Set ( fixtures . map (( item ) => item . businessDate ). filter ( Boolean ))
if ( distinctBusinessDates . size < 2 ) blocker ( context , '费用来源覆盖不同业务日期' ,
`businessDates= ${ [... distinctBusinessDates ]. join ( ',' ) || 'none' } ;至少需要两个真实日期以验收按日期排除` )
2026-08-12 14:56:50 +08:00
context . assert ( MODULE , '取得订单真实运单标识' , null ,
measured . length > 0 && measured . every (( item ) => item . waybillId || item . shipmentId || item . orderNo ),
'使用订单域真实 waybillId、shipmentId 或 orderNo,不使用数据库主键冒充运单标识' , OPTIONS )
2026-08-15 17:45:51 +08:00
const fixtureOrders = measured . map (( item ) => ({ orderId : id ( item . orderId ), orderNo : item . orderNo || null ,
shipmentId : item . shipmentId ? id ( item . shipmentId ) : null ,
waybillId : item . waybillId ? id ( item . waybillId ) : ( item . shipmentId ? id ( item . shipmentId ) : null ), inboundId : id ( item . inboundId ),
actualWeightKg : item . actualWeightKg , totalVolumeCbm : item . totalVolumeCbm ,
businessDate : fixtures . find (( fee ) => fee . orderId === id ( item . orderId )) ? . businessDate || null }))
const fixtureSources = normalizeFinanceFixtureSources ( fixtures , fixtureOrders , allocations , customerId , context . marker )
const invalidFixtureSources = fixtureSources . filter (( source ) => ! source . feeSourceId || ! source . customerId
2026-08-15 17:52:46 +08:00
|| ! source . businessDate || ! ( source . waybillId || source . shipmentId || source . orderNo ) || source . marker !== context . marker
|| ( source . direction === 'PAYABLE' && ( ! source . supplierId || ! source . supplierName
2026-08-20 17:07:51 +08:00
|| ( payableSupplierCategory && source . supplierCategory !== payableSupplierCategory ))))
2026-08-15 17:45:51 +08:00
context . assert ( MODULE , '输出完整 Finance 来源契约' , null , invalidFixtureSources . length === 0 ,
`sources= ${ fixtureSources . length } , invalid= ${ invalidFixtureSources . length } ;字段必须来自本轮费用来源、客户、订单/运单和真实柜次分摊` , OPTIONS )
2026-08-12 10:59:39 +08:00
context . createdResources . financeFullFixtures = {
marker : context . marker , customerId ,
2026-08-13 14:37:51 +08:00
prerequisites : {
container : containerOwned && Boolean ( containerFixture ? . feeSourceId ) && allocations . length > 0
? { ready : true , category : null }
: { ready : false , category : 'container-prerequisite' , evidence : '缺少同 marker 已完成柜次、有效 containerBookingId、按柜费用来源或真实分摊' },
2026-08-15 17:45:51 +08:00
financeSources : fixtureSources . length > 0 && invalidFixtureSources . length === 0
2026-08-13 14:37:51 +08:00
? { ready : true , category : null }
2026-08-15 17:45:51 +08:00
: { ready : false , category : 'finance-source-prerequisite' , evidence : '本轮 marker 未产生字段完整的可消费 Finance 费用来源' },
2026-08-13 14:37:51 +08:00
},
2026-08-15 17:45:51 +08:00
orders : fixtureOrders ,
feeSources : fixtureSources ,
2026-08-12 10:59:39 +08:00
container : containerFixture ? { containerBookingId : containerFixture . containerBookingId ,
containerNo : containerFixture . containerNo } : { containerBookingId : null , containerNo : null },
2026-08-19 09:42:07 +08:00
allocations , supplier : supplier ? . id ? { supplierId : id ( supplier . id ),
2026-08-15 17:52:46 +08:00
supplierName : supplier . name || null , category : supplier . category || payableSupplierCategory } : null ,
2026-08-12 10:59:39 +08:00
voucherFileId , pricingEvidence ,
deploymentBlocker : context . steps . slice ( start ). filter (( step ) => step . deploymentBlocker ). map (( step ) => step . evidence ),
}
const steps = context . steps . slice ( start )
context . scenarios . push ({ name : OPTIONS . scenario , module : MODULE , success : steps . every (( step ) => step . passed ), deploymentBlocked : steps . some (( step ) => step . deploymentBlocker ), passed : steps . filter (( step ) => step . passed ). length , failed : steps . filter (( step ) => ! step . passed ). length })
}