@@ -0,0 +1,479 @@
#!/usr/bin/env node
// Explicitly authorized non-production HTTP import. Never resets a database.
import { createHash } from 'node:crypto'
import { execFileSync } from 'node:child_process'
import { mkdir , readFile , writeFile , rename , open } from 'node:fs/promises'
import { resolve } from 'node:path'
import { pathToFileURL } from 'node:url'
import { AcceptanceRun , parseArguments , validateExecutionOptions } from './admin-test-env-acceptance.mjs'
const hash = ( value ) => createHash ( 'sha256' ) . update ( value ) . digest ( 'hex' )
const text = ( value ) => String ( value ? ? '' ) . trim ( )
const round = ( value , digits = 6 ) => Number ( value . toFixed ( digits ) )
const positive = ( value ) => value !== null && value !== '' && Number . isFinite ( Number ( value ) ) && Number ( value ) > 0
const PNG = Buffer . from ( 'iVBORw0KGgoAAAANSUhEUgAAAAEAAAABCAQAAAC1HAwCAAAAC0lEQVR42mNk+A8AAQUBAScY42YAAAAASUVORK5CYII=' , 'base64' )
const PALLETS = [ 'TP-001' , 'YW-TP-002' ]
const declarationMode = ( value ) => ( { 买单 : 'BUY_DECLARATION' , 退税 : 'TAX_REFUND' ,
买单改退税 : 'TAX_REFUND' , 退税改买单 : 'BUY_DECLARATION' } ) [ text ( value ) ] || null
export function channelName ( value ) {
return text ( value ) . replace ( /\s/g , '' ) . replace ( /[( (](?:FEDEX|UPS)[) )]/gi , '' ) . replace ( /不包税|包税/g , '' )
}
export function normalizeRows ( rows ) {
const header = rows [ 0 ] . map ( ( value ) => text ( value ) . replace ( /\s/g , '' ) )
const required = [ '进仓编号' , '客户' , '渠道' , '亚马逊仓' , '实际件数' , '实际毛重' , '实际立方' , '预报件数' ]
for ( const name of required ) if ( ! header . includes ( name ) ) throw Error ( ` 缺少源表列: ${ name } ` )
const get = ( row , name ) => row [ header . indexOf ( name ) ]
const groups = new Map ( ) , excluded = [ ]
rows . slice ( 1 ) . forEach ( ( row , offset ) => {
if ( row . every ( ( value ) => value === null || value === '' ) ) return
const code = text ( get ( row , '进仓编号' ) ) . match ( /^[A-Z]{2,8}\d{8,16}(?=\s|$)/ ) ? . [ 0 ]
if ( ! code ) { excluded . push ( { rows : [ offset + 2 ] , reason : 'INVALID_SOURCE_CODE' } ) ; return }
const group = groups . get ( code ) || [ ]
group . push ( { row , rowNumber : offset + 2 } ) ; groups . set ( code , group )
} )
const orders = [ ... groups ] . map ( ( [ sourceCode , entries ] ) => {
const first = entries [ 0 ] . row
const distinct = ( name ) => [ ... new Set ( entries . map ( ( { row } ) => text ( get ( row , name ) ) ) ) ]
const problems = [ ]
for ( const name of [ '客户' , '渠道' , '亚马逊仓' ] ) {
if ( distinct ( name ) . length !== 1 ) problems . push ( ` CONFLICT_ ${ name } ` )
}
if ( new Set ( entries . map ( ( { row } ) => declarationMode ( get ( row , '买单/退税' ) ) ) ) . size !== 1 ) problems . push ( 'CONFLICT_买单/退税' )
// Missing/zero actuals do not silently become forecast facts.
for ( const name of [ '实际件数' , '实际毛重' , '实际立方' ] ) {
if ( entries . some ( ( { row } ) => ! positive ( get ( row , name ) ) ) ) problems . push ( ` MISSING_ ${ name } ` )
}
const sum = ( name ) => round ( entries . reduce ( ( total , { row } ) => total + ( Number ( get ( row , name ) ) || 0 ) , 0 ) )
const packageCount = sum ( '实际件数' )
if ( ! Number . isInteger ( packageCount ) || packageCount < 1 ) problems . push ( 'INVALID_PACKAGE_COUNT' )
if ( entries . length > 1 ) {
const forecasts = distinct ( '预报件数' ) . map ( ( value ) => Number ( value . match ( /^\d+/ ) ? . [ 0 ] ) )
if ( forecasts . length !== 1 || forecasts [ 0 ] !== packageCount ) problems . push ( 'SPLIT_COUNT_MISMATCH' )
}
const sourceChannel = text ( get ( first , '渠道' ) )
const destination = text ( get ( first , '亚马逊仓' ) )
const taxMode = declarationMode ( get ( first , '买单/退税' ) )
if ( ! taxMode ) problems . push ( 'UNKNOWN_DECLARATION_MODE' )
const pricingTaxMode = sourceChannel . includes ( '不包税' ) ? 'TAX_EXCLUDED'
: sourceChannel . includes ( '包税' ) ? 'TAX_INCLUDED' : null
if ( ! pricingTaxMode ) problems . push ( 'UNKNOWN_PRICING_TAX_MODE' )
const chargeModes = entries . map ( ( { row } ) => ( { KGS : 'KG' , KG : 'KG' , CBM : 'CBM' } ) [ text ( get ( row , '单位' ) ) . toUpperCase ( ) ] )
const chargeMode = pricingTaxMode === 'TAX_INCLUDED' ? 'KG' : chargeModes [ 0 ]
if ( pricingTaxMode === 'TAX_EXCLUDED' && ( ! chargeMode || chargeModes . some ( ( mode ) => mode !== chargeMode ) ) ) problems . push ( 'UNKNOWN_OR_CONFLICTING_CHARGE_MODE' )
const destinationType = destination . includes ( '私人' ) ? 'PRIVATE'
: /^[A-Z]{3}\d$/ . test ( destination ) ? 'FBA' : 'OVERSEAS'
return { key : hash ( sourceCode ) . slice ( 0 , 24 ) , sourceCode , rows : entries . map ( ( entry ) => entry . rowNumber ) ,
sourceCustomer : text ( get ( first , '客户' ) ) , sourceChannel , channelName : channelName ( sourceChannel ) ,
destination , destinationType , pricingTaxMode , taxMode , chargeMode ,
fulfillmentType : destination . includes ( '一件代发' ) ? 'ONE_CLICK_DROPSHIP' : 'STANDARD' ,
packageCount , weightKg : sum ( '实际毛重' ) , volumeCbm : sum ( '实际立方' ) ,
cargoName : distinct ( '品名' ) . filter ( Boolean ) . join ( ' / ' ) ,
sku : distinct ( 'SKU' ) . filter ( Boolean ) . join ( '\n' ) , shipmentId : distinct ( 'FBA货件编号' ) . filter ( Boolean ) . join ( '\n' ) ,
referenceId : distinct ( '跟踪编号' ) . filter ( Boolean ) . join ( '\n' ) , problems }
} )
return { orders , excluded }
}
export function matchRoute ( order , catalog ) {
if ( order . problems . length ) throw Error ( order . problems . join ( '|' ) )
if ( order . destinationType === 'PRIVATE' ) throw Error ( 'PRIVATE_ADDRESS_REQUIRES_EXPLICIT_MAPPING' )
// Do not create work pallets to bypass the user's no-new-pallet constraint.
if ( order . fulfillmentType === 'ONE_CLICK_DROPSHIP' ) throw Error ( 'DROPSHIP_REQUIRES_WORK_PALLET_WORKFLOW' )
const channels = catalog . channels . filter ( ( item ) => item . channelName === order . channelName
&& item . pricingTaxModes ? . includes ( order . pricingTaxMode ) )
if ( channels . length !== 1 ) throw Error ( 'CHANNEL_OR_TAX_MODE_NOT_UNIQUE' )
const destinations = catalog . destinations . filter ( ( item ) => item . warehouseType === order . destinationType
&& ( item . warehouseCode === order . destination || item . warehouseName === order . destination ) )
if ( destinations . length !== 1 ) throw Error ( 'DESTINATION_NOT_UNIQUE' )
return { channel : channels [ 0 ] , destination : destinations [ 0 ] , receiving : catalog . receiving }
}
export function measurementShape ( order ) {
if ( ! positive ( order . volumeCbm ) || ! positive ( order . weightKg ) || ! Number . isInteger ( order . packageCount )
|| order . packageCount < 1 ) throw Error ( 'INVALID_MEASUREMENT_FACTS' )
const edge = round ( Math . cbrt ( order . volumeCbm * 1e6 / order . packageCount ) , 3 )
return { length : edge , width : edge , height : edge , palletCount : Math . ceil ( order . packageCount / 30 ) }
}
export function totalPreservingShapes ( order , rules ) {
measurementShape ( order )
if ( ! Array . isArray ( rules ) || rules . length !== 4 || rules . some ( ( r ) =>
! [ r . innerLengthCm , r . innerWidthCm , r . innerHeightCm , r . edgeAdditionCm ] . every ( positive ) ) ) throw Error ( 'INVALID_DIMENSION_RULES' )
const sorted = [ ... rules ] . sort ( ( a , b ) => a . innerLengthCm * a . innerWidthCm * a . innerHeightCm
- b . innerLengthCm * b . innerWidthCm * b . innerHeightCm )
const choose = ( edge ) => sorted . find ( ( r ) => Math . min ( r . innerLengthCm , r . innerWidthCm , r . innerHeightCm ) >= edge ) || sorted . at ( - 1 )
// Manual outer dimensions use server CEILING(4) per carton. Distribute the
// integer 0.0001 m3 remainder across two groups, not independently rounded boxes.
const scale = Math . abs ( Math . round ( order . volumeCbm * 1000 ) / 1000 - order . volumeCbm ) < 0.0000001 ? 1000 : 10000
const ticks = Math . round ( order . volumeCbm * scale )
if ( Math . abs ( ticks / scale - order . volumeCbm ) > 0.0000001 ) throw Error ( 'SOURCE_VOLUME_EXCEEDS_SERVER_PRECISION' )
const lower = Math . floor ( ticks / order . packageCount ) , extra = ticks % order . packageCount
const groups = [ { count : order . packageCount - extra , ticks : lower } , { count : extra , ticks : lower + 1 } ] . filter ( ( g ) => g . count )
return groups . map ( ( group ) => {
if ( group . ticks < 1 ) throw Error ( 'VOLUME_TOO_SMALL_FOR_CARTON_COUNT' )
const unitVolumeCbm = group . ticks / scale
for ( const rule of sorted ) {
const addition = 2 * Number ( rule . edgeAdditionCm )
const edge = round ( Math . cbrt ( ( unitVolumeCbm - 0.00005 ) * 1e6 ) - addition , 3 )
const raw = round ( ( edge + addition ) * * 3 / 1e6 , 6 )
const serverTicks = Math . ceil ( raw * 10000 - 1e-8 )
if ( edge > 0 && choose ( edge ) === rule && serverTicks === Math . round ( unitVolumeCbm * 10000 ) ) {
return { length : edge , width : edge , height : edge , count : group . count ,
unitVolumeCbm , edgeAdditionCm : Number ( rule . edgeAdditionCm ) , boxType : rule . boxType }
}
}
throw Error ( 'NO_TOTAL_PRESERVING_DIMENSIONS_FOR_CURRENT_RULES' )
} )
}
function oneValue ( value , fallback , max ) {
// Multiline/oversized identifiers are not silently truncated into a different real ID.
return value && ! /[\r\n]/ . test ( value ) && value . length <= max ? value : fallback
}
export function buildPayload ( order , route , customer , imageId , marker ) {
const shape = measurementShape ( order )
const shipmentId = oneValue ( order . shipmentId , ` TEST-SHIP- ${ marker } ` , 64 )
const referenceId = oneValue ( order . referenceId , ` TEST-REF- ${ marker } ` , 64 )
const fba = order . destinationType === 'FBA'
return {
customerName : customer , transportMode : 'SEA' , tradeTerm : 'FOB' , fulfillmentType : 'STANDARD' ,
pickupMethod : 'SELF_EXPRESS' , receivingWarehouseId : route . receiving . id ,
receivingWarehouseName : route . receiving . warehouseName ,
channelCode : route . channel . channelCode , channelName : route . channel . channelName ,
pricingTaxMode : order . pricingTaxMode , taxMode : order . taxMode , containerMode : 'LCL' ,
customsInspection : false , destinationType : order . destinationType ,
destinationWarehouseId : route . destination . id , destinationWarehouseName : route . destination . warehouseName ,
destinationCountryCode : route . destination . countryCode , amazonWarehouseCode : fba ? route . destination . warehouseCode : null ,
shipmentId : fba ? shipmentId : null , referenceId : fba ? referenceId : null ,
customerRemark : ` ${ marker } 测试导入,源入仓码= ${ order . sourceCode } ;源客户= ${ order . sourceCustomer } ;源渠道= ${ order . sourceChannel } ;箱数/重量/体积取源实际值;箱规为等体积推算,照片及缺失产品字段为测试占位;非真实测量证据 ` . slice ( 0 , 1000 ) ,
cargoes : [ { cargoName : oneValue ( order . cargoName , '测试导入汇总货物(详见源表)' , 128 ) ,
cargoNameEn : 'TEST imported goods' , skuNo : oneValue ( order . sku , ` TEST-SKU- ${ marker } ` , 64 ) ,
packageType : 'CARTON' , packageCount : order . packageCount , grossWeightKg : order . weightKg ,
volumeCbm : order . volumeCbm , lengthCm : shape . length , widthCm : shape . width , heightCm : shape . height ,
unitsPerPackage : 1 , totalQuantity : order . packageCount , netWeightKg : order . weightKg ,
hsCode : '0000000000' , customsHsCode : '0000000000' , unitDeclaredValue : 1 ,
declaredValue : order . packageCount , currency : 'USD' , material : 'TEST_UNKNOWN' , usage : '测试流程' ,
model : 'TEST' , brand : 'TEST' , productLengthCm : 1 , productWidthCm : 1 , productHeightCm : 1 ,
shipmentId : fba ? shipmentId : null , referenceId : fba ? referenceId : null ,
upcNo : '000000000000' , productImageFileId : imageId , dangerousGoods : false , woodenProduct : false ,
cargoRemark : ` ${ marker } 聚合源表行 ${ order . rows . join ( ',' ) } ;商品数量/申报价/材质/HS/属性为测试占位,禁止用于真实报关 ` ,
} ] ,
}
}
export function buildSourceDraft ( order , route , customer , marker ) {
// Drafts deliberately retain missing fields. No inferred dimensions, fake photo,
// customs declaration values or dangerous-goods assertions are submitted here.
// User-confirmed import rule: absent wooden-product information means false.
return {
customerName : customer , transportMode : 'SEA' , fulfillmentType : order . fulfillmentType ,
pickupMethod : 'SELF_EXPRESS' , receivingWarehouseId : route . receiving . id ,
receivingWarehouseName : route . receiving . warehouseName ,
channelCode : route . channel . channelCode , channelName : route . channel . channelName ,
pricingTaxMode : order . pricingTaxMode , taxMode : order . taxMode , destinationType : order . destinationType ,
destinationWarehouseId : route . destination . id , destinationWarehouseName : route . destination . warehouseName ,
destinationCountryCode : route . destination . countryCode ,
amazonWarehouseCode : order . destinationType === 'FBA' ? route . destination . warehouseCode : null ,
customerRemark : ` ${ marker } 源数据草稿;源入仓码= ${ order . sourceCode } ;源客户= ${ order . sourceCustomer } ;箱数/重量/体积取源实际值。缺失产品字段和实测证据待补充。 ` . slice ( 0 , 1000 ) ,
cargoes : [ { cargoName : oneValue ( order . cargoName , '' , 128 ) , skuNo : oneValue ( order . sku , null , 64 ) ,
packageType : 'CARTON' , woodenProduct : false , packageCount : order . packageCount , grossWeightKg : order . weightKg ,
volumeCbm : order . volumeCbm , shipmentId : oneValue ( order . shipmentId , null , 64 ) ,
referenceId : oneValue ( order . referenceId , null , 64 ) ,
cargoRemark : ` ${ marker } 源表行 ${ order . rows . join ( ',' ) } ;多货物原文请核对源表 ` } ] ,
}
}
export function requireSuccess ( response ) {
if ( response ? . http !== 200 || String ( response ? . body ? . code ) !== '200' ) {
const error = Error ( ` HTTP_ ${ response ? . http } _CODE_ ${ response ? . body ? . code || 'MISSING' } ` )
error . evidence = { method : response ? . method , path : response ? . path , http : response ? . http ,
code : response ? . body ? . code , message : response ? . body ? . message ,
requestId : response ? . requestId , traceId : response ? . traceId }
throw error
}
return response . body . data
}
async function main ( ) {
const args = new Map ( )
const allowed = new Set ( [ 'input' , 'sheet' , 'python' , 'output-dir' , 'batch-id' , 'stage' , 'limit' , 'customer-name' , 'origin-warehouse-name' , 'allow-test-placeholders' , 'expected-host' ] )
for ( let i = 2 ; i < process . argv . length ; i += 2 ) {
const name = process . argv [ i ] . replace ( /^--/ , '' ) , value = process . argv [ i + 1 ]
if ( ! allowed . has ( name ) || ! value || value . startsWith ( '--' ) ) throw Error ( ` 无效参数: ${ name } ` )
args . set ( name , value )
}
for ( const key of [ 'input' , 'python' , 'output-dir' , 'batch-id' , 'expected-host' ] ) if ( ! args . get ( key ) ) throw Error ( ` 缺少 -- ${ key } ` )
const stage = args . get ( 'stage' ) || 'preflight'
if ( ! [ 'preflight' , 'create' , 'wait-split' ] . includes ( stage ) ) throw Error ( 'stage 必须为 preflight/create/wait-split' )
if ( stage === 'wait-split' && args . get ( 'allow-test-placeholders' ) !== 'true' ) throw Error ( '此源表缺少必填字段,模拟测量须显式允许测试占位' )
const limit = Number ( args . get ( 'limit' ) || 1 )
if ( ! Number . isInteger ( limit ) || limit < 1 ) throw Error ( 'limit 必须为正整数' )
const options = validateExecutionOptions ( parseArguments ( [ '--modules' , 'orders,inbound-measurement' , '--quiet' , 'true' ] ) )
if ( new URL ( options . gateway ) . hostname !== args . get ( 'expected-host' ) || ! /^test[-.]/ . test ( args . get ( 'expected-host' ) ) ) throw Error ( '只允许明确匹配的测试域名' )
const input = resolve ( args . get ( 'input' ) )
const bytes = await readFile ( input )
const rows = JSON . parse ( execFileSync ( args . get ( 'python' ) , [ '-c' ,
'import openpyxl,json,sys; w=openpyxl.load_workbook(sys.argv[1],read_only=True,data_only=True); print(json.dumps(list(w[sys.argv[2]].values),ensure_ascii=False,default=str)); w.close()' ,
input , args . get ( 'sheet' ) || 'D1512开始' ] , { encoding : 'utf8' , maxBuffer : 50 * 1024 * 1024 ,
env : { ... process . env , PYTHONIOENCODING : 'utf-8' } } ) )
const normalized = normalizeRows ( rows )
const directory = resolve ( args . get ( 'output-dir' ) )
await mkdir ( directory , { recursive : true } )
const statePath = resolve ( directory , 'state.json' )
const identity = { sourceHash : hash ( bytes ) , batchId : args . get ( 'batch-id' ) , gateway : hash ( options . gateway ) ,
customer : args . get ( 'customer-name' ) || 'IT测试' , origin : args . get ( 'origin-warehouse-name' ) || '义乌仓' }
let state
try { state = JSON . parse ( await readFile ( statePath , 'utf8' ) ) } catch ( error ) { if ( error . code !== 'ENOENT' ) throw error }
if ( state && JSON . stringify ( state . identity ) !== JSON . stringify ( identity ) ) throw Error ( '批次/源文件/目标发生变化,请使用独立输出目录' )
state || = { identity , orders : { } , startedAt : new Date ( ) . toISOString ( ) }
const lock = await open ( resolve ( directory , 'run.lock' ) , 'wx' )
const save = async ( ) => {
await writeFile ( ` ${ statePath } .tmp ` , JSON . stringify ( state , null , 2 ) , 'utf8' )
for ( let attempt = 0 ; ; attempt ++ ) {
try { await rename ( ` ${ statePath } .tmp ` , statePath ) ; break }
catch ( error ) {
if ( ! [ 'EPERM' , 'EBUSY' , 'EACCES' ] . includes ( error . code ) || attempt >= 5 ) throw error
await new Promise ( ( resolve ) => setTimeout ( resolve , 100 * ( attempt + 1 ) ) )
}
}
}
const ctx = new AcceptanceRun ( options )
const api = async ( method , path , body , token = ctx . token , headers = { } ) => requireSuccess ( await ctx . request ( method , path , body , token , headers ) )
try {
ctx . token = ( await api ( 'POST' , '/api/auth/login' , { loginId : options . username , password : options . password } , '' ) ) . accessToken
const merchant = ( await api ( 'POST' , '/api/auth/login' , { loginId : process . env . MERCHANT _TEST _USERNAME , password : process . env . MERCHANT _TEST _PASSWORD } , '' ) ) . accessToken
if ( ! ctx . token || ! merchant ) throw Error ( 'LOGIN_TOKEN_MISSING' )
const profile = await api ( 'GET' , '/api/merchant/v1/iam/tenant/profile' , undefined , merchant )
if ( profile . tenantName !== identity . customer || profile . status !== 'ACTIVE' ) throw Error ( 'CUSTOMER_MISMATCH_OR_INACTIVE' )
const list = async ( type ) => {
const result = [ ]
for ( let page = 1 ; ; page ++ ) {
const data = await api ( 'GET' , ` /api/merchant/v1/warehouses?page= ${ page } &size=200&status=ACTIVE&warehouseType= ${ type } ` , undefined , merchant )
if ( ! Array . isArray ( data . records ) || ! Number . isFinite ( Number ( data . total ) ) ) throw Error ( 'WAREHOUSE_PAGE_INVALID' )
result . push ( ... data . records )
if ( result . length >= Number ( data . total ) ) return result
if ( ! data . records . length || page >= 100 ) throw Error ( 'WAREHOUSE_PAGE_INCOMPLETE' )
}
}
const origins = ( await list ( 'DOMESTIC' ) ) . filter ( ( item ) => item . warehouseName === identity . origin )
if ( origins . length !== 1 ) throw Error ( 'ORIGIN_NOT_UNIQUE' )
const receiving = origins [ 0 ]
const channels = await api ( 'GET' , ` /api/merchant/v1/first-leg/pricing/channels?originWarehouseId= ${ receiving . id } ` , undefined , merchant )
const catalog = { receiving , channels , destinations : [ ... await list ( 'FBA' ) , ... await list ( 'OVERSEAS' ) ] }
const pallets = [ ]
for ( const code of PALLETS ) {
const page = await api ( 'GET' , ` /api/platform/v1/pallets?page=1&size=20&tenantId=0&warehouseId= ${ receiving . id } &palletCode= ${ encodeURIComponent ( code ) } ` )
const found = page . records ? . filter ( ( item ) => item . palletCode === code && String ( item . warehouseId ) === String ( receiving . id ) )
if ( found ? . length !== 1 ) throw Error ( ` REQUIRED_PALLET_MISSING_ ${ code } ` )
pallets . push ( found [ 0 ] )
}
const policy = await api ( 'GET' , '/api/merchant/v1/orders/first-leg/inbound-code-policy' , undefined , merchant )
if ( policy . mode !== 'AUTO' ) throw Error ( 'INBOUND_CODE_POLICY_NOT_AUTO' )
const candidates = [ ] , rejected = [ ... normalized . excluded ]
for ( const order of normalized . orders ) {
try { candidates . push ( { order , route : matchRoute ( order , catalog ) } ) }
catch ( error ) { rejected . push ( { key : order . key , rows : order . rows , reason : error . message } ) }
}
const preflight = { sourceRows : rows . length - 1 , orders : normalized . orders . length ,
matched : candidates . length , rejected , channelCount : channels . length , palletCodes : PALLETS ,
note : '匹配仅验证目录,写入前还会用实际重量体积试价。私人仓、需工作托盘的一件代发及歧义源记录不擅自替代。' }
await writeFile ( resolve ( directory , 'preflight.json' ) , JSON . stringify ( preflight , null , 2 ) , 'utf8' )
console . log ( JSON . stringify ( { stage , sourceRows : preflight . sourceRows , orders : preflight . orders , matched : candidates . length , rejected : rejected . length } ) )
await save ( )
if ( stage === 'preflight' ) return
await executeCandidates ( { candidates , limit , state , save , api , ctx , merchant , profile , pallets , stage } )
} finally {
await lock . close ( )
const { unlink } = await import ( 'node:fs/promises' )
await unlink ( resolve ( directory , 'run.lock' ) )
}
}
export async function executeCandidates ( { candidates , limit , state , save , api , ctx , merchant , profile , pallets , stage } ) {
let attempted = 0 , succeeded = 0 , failed = 0 , skipped = 0
for ( const { order , route } of candidates ) {
// An interrupted write is not permission to create another order. Keep its last
// checkpoint for inspection. A reset database is always a NEW batch/directory.
if ( state . orders [ order . key ] ) { skipped ++ ; continue }
if ( attempted >= limit ) break
const marker = ` XLSX- ${ hash ( ` ${ state . identity . batchId } : ${ order . key } ` ) . slice ( 0 , 24 ) } `
const entry = { rows : order . rows , marker , status : 'STARTED' , stage : 'estimate' , startedAt : new Date ( ) . toISOString ( ) }
state . orders [ order . key ] = entry
await save ( )
attempted ++
try {
const estimate = await api ( 'POST' , '/api/merchant/v1/first-leg/pricing/estimate' , {
originWarehouseId : route . receiving . id , channelCode : route . channel . channelCode ,
destinationCode : route . destination . warehouseCode , destinationType : order . destinationType ,
destinationWarehouseId : route . destination . id , destinationName : route . destination . warehouseName ,
pricingTaxMode : order . pricingTaxMode , chargeMode : order . chargeMode ,
actualWeightKg : order . weightKg , actualVolumeCbm : order . volumeCbm ,
} , merchant )
if ( estimate ? . channelCode !== route . channel . channelCode || estimate ? . pricingTaxMode !== order . pricingTaxMode ) throw Error ( 'ESTIMATE_ROUTE_MISMATCH' )
let payload
if ( stage === 'create' ) {
payload = buildSourceDraft ( order , route , profile . tenantName , marker )
} else {
const config = await api ( 'GET' , ` /api/platform/v1/first-leg/customer-dimension-config?tenantId= ${ profile . id } ` )
entry . dimensionPlan = totalPreservingShapes ( order , config . rules )
entry . stage = 'product-image' ; await save ( )
const image = requireSuccess ( await ctx . requestMultipart ( 'POST' , '/api/merchant/v1/files/product-images' ,
{ content : PNG , type : 'image/png' , name : ` ${ marker } -TEST.png ` } , merchant ) )
if ( ! image ? . fileId ) throw Error ( 'PRODUCT_IMAGE_ID_MISSING' )
entry . productImageFileId = image . fileId
payload = buildPayload ( order , route , profile . tenantName , image . fileId , marker )
}
entry . stage = 'create' ; entry . payloadHash = hash ( JSON . stringify ( payload ) ) ; await save ( )
entry . orderId = String ( await api ( 'POST' , '/api/merchant/v1/orders/first-leg' , payload , merchant ,
{ 'Idempotency-Key' : ` ${ marker } -create ` } ) )
if ( ! entry . orderId || entry . orderId === 'null' || entry . orderId === 'undefined' ) throw Error ( 'ORDER_ID_MISSING' )
entry . stage = 'draft-created' ; await save ( )
const clientPath = ` /api/merchant/v1/orders/first-leg/ ${ entry . orderId } `
const draft = await api ( 'GET' , clientPath , undefined , merchant )
if ( String ( draft . order ? . id ) !== entry . orderId || ! draft . order ? . customerRemark ? . includes ( marker ) ) throw Error ( 'ORDER_PROVENANCE_MISMATCH' )
const cargo = draft . cargoes ? . [ 0 ]
if ( draft . cargoes ? . length !== 1 || Number ( cargo . packageCount ) !== order . packageCount
|| Math . abs ( Number ( cargo . grossWeightKg ) - order . weightKg ) > 0.001
|| Math . abs ( Number ( cargo . volumeCbm ) - order . volumeCbm ) > 0.00001 ) throw Error ( 'DRAFT_SOURCE_FACTS_MISMATCH' )
entry . orderNo = draft . order . orderNo
entry . declarationVerified = true
if ( stage === 'wait-split' ) {
entry . stage = 'submit' ; await save ( )
await api ( 'POST' , ` ${ clientPath } /submit ` , undefined , merchant , { 'Idempotency-Key' : ` ${ marker } -submit ` } )
entry . stage = 'approve' ; await save ( )
const platformPath = ` /api/platform/v1/orders/first-leg/ ${ entry . orderId } `
const q = ` tenantId= ${ profile . id } `
await api ( 'POST' , ` ${ platformPath } /approve? ${ q } ` )
const unified = await poll ( async ( ) => api ( 'GET' , ` /api/platform/v1/orders/ ${ entry . orderId } ? ${ q } ` ) ,
( data ) => data . summary ? . inboundId && [ 'WAIT_MEASURE' , 'MEASURING' ] . includes ( data . summary ? . mainStatus ) )
entry . inboundId = String ( unified . summary . inboundId )
entry . stage = 'inbound-created' ; await save ( )
await api ( 'POST' , ` ${ platformPath } /accept? ${ q } ` )
await api ( 'POST' , ` ${ platformPath } /start? ${ q } ` )
await measureOrder ( { order , entry , ctx , api , pallets , tenantId : String ( profile . id ) , save } )
}
entry . status = 'SUCCEEDED' ; succeeded ++
} catch ( error ) {
entry . status = 'FAILED' ; entry . error = error . message ; entry . evidence = error . evidence ; failed ++
}
entry . finishedAt = new Date ( ) . toISOString ( ) ; await save ( )
console . log ( JSON . stringify ( { attempted , succeeded , failed , skipped , rows : entry . rows ,
stage : entry . stage , status : entry . status , orderNo : entry . orderNo , inboundCode : entry . inboundCode , error : entry . error , evidence : entry . evidence } ) )
// A small pilot must expose a common deployment/data defect before multiplying it.
if ( failed >= 3 && succeeded === 0 ) break
}
state . lastRun = { attempted , succeeded , failed , skipped , stage , finishedAt : new Date ( ) . toISOString ( ) }
await save ( )
if ( failed ) process . exitCode = 1
}
async function poll ( read , predicate ) {
let data
for ( let i = 0 ; i < 30 ; i ++ ) {
data = await read ( )
if ( predicate ( data ) ) return data
if ( i < 29 ) await new Promise ( ( resolve ) => setTimeout ( resolve , 1000 ) )
}
const error = Error ( 'PROJECTION_NOT_READY' )
error . evidence = { processStatus : data ? . processStatus || data ? . summary ? . mainStatus ,
measurementStatus : data ? . measurementStatus , measureReviewStatus : data ? . measureReviewStatus }
throw error
}
async function measureOrder ( { order , entry , ctx , api , pallets , tenantId , save } ) {
const inboundId = entry . inboundId , q = ` tenantId= ${ tenantId } ` , marker = entry . marker
const inboundPath = ` /api/platform/inbounds/ ${ inboundId } ? ${ q } `
const base = ` /api/platform/v1/first-leg/inbounds/ ${ inboundId } `
const measurementPath = ` ${ base } /measurement? ${ q } `
const inbound = await api ( 'GET' , inboundPath )
if ( String ( inbound . summary ? . tenantId ) !== tenantId || String ( inbound . summary ? . id ) !== inboundId ) throw Error ( 'INBOUND_PROVENANCE_MISMATCH' )
entry . inboundCode = inbound . summary . inboundCode
entry . stage = 'carton-labels' ; await save ( )
const pdf = await ctx . requestBinary ( 'POST' , ` /api/platform/inbounds/ ${ inboundId } /carton-label-pdf? ${ q } ` )
if ( pdf . http !== 200 || ! pdf . contentType ? . includes ( 'pdf' ) || Buffer . from ( pdf . bytes || [ ] ) . subarray ( 0 , 4 ) . toString ( ) !== '%PDF' ) throw Error ( 'LABEL_GENERATION_FAILED' )
const shape = measurementShape ( order )
entry . stage = 'carton-scan' ; await save ( )
let cartonIndex = 1
for ( const group of entry . dimensionPlan ) {
const form = new FormData ( )
form . append ( 'data' , new Blob ( [ JSON . stringify ( { boxCode : ` ${ entry . inboundCode } # ${ order . packageCount } - ${ cartonIndex } ` ,
length : group . length , width : group . width , height : group . height ,
machineNo : 'TEST-XLSX-SIMULATED' , measuredAt : new Date ( ) . toISOString ( ) . slice ( 0 , 19 ) } ) ] , { type : 'application/json' } ) )
form . append ( 'image' , new Blob ( [ PNG ] , { type : 'image/png' } ) , 'TEST-simulated-carton.png' )
const scan = requireSuccess ( await ctx . requestMultipart ( 'POST' , ` ${ base } /measurement/carton-scans? ${ q } ` , form ) )
if ( ! scan ? . itemId || ! scan ? . measurementId ) throw Error ( 'SCAN_IDS_MISSING' )
entry . measurementId = scan . measurementId ; await save ( )
await api ( 'PUT' , ` ${ base } /measurement/items/ ${ scan . itemId } /confirmed-package-count? ${ q } ` ,
{ measurementVersion : Number ( scan . measurementVersion ) , confirmedPackageCount : group . count } )
group . itemId = String ( scan . itemId )
cartonIndex += group . count
}
const measured = await api ( 'GET' , measurementPath )
if ( measured . items ? . length !== entry . dimensionPlan . length ) throw Error ( 'UNEXPECTED_MEASUREMENT_ITEMS' )
const draft = await api ( 'PUT' , ` ${ base } /measurement/draft? ${ q } ` , {
measurementId : measured . measurementId , version : Number ( measured . version ) ,
actualGoodsName : oneValue ( order . cargoName , '测试导入汇总货物' , 128 ) ,
items : entry . dimensionPlan . map ( ( group ) => {
const item = measured . items . find ( ( value ) => String ( value . id ) === group . itemId )
if ( ! item ) throw Error ( 'MEASUREMENT_GROUP_MISSING' )
return { inboundDetailId : item . inboundDetailId , itemNo : item . itemNo , boxSpecification : item . boxSpecification ,
packageCount : group . count , lengthCm : group . length , widthCm : group . width , heightCm : group . height ,
intelligentImageFileId : item . intelligentImageFileId , overLimitWarning : item . overLimitWarning ? ? null ,
outerLengthCm : group . length , outerWidthCm : group . width , outerHeightCm : group . height ,
coefficientMode : 'ADD' ,
remark : ` ${ marker } TEST按总体积分组反算箱规,非实测 ` } } ) ,
} )
const draftCheck = await api ( 'GET' , measurementPath )
if ( Math . abs ( Number ( draftCheck . actualVolumeCbm ) - order . volumeCbm ) > 0.0000001
|| Number ( draftCheck . actualPackageCount ) !== order . packageCount ) throw Error ( 'DRAFT_VOLUME_OR_COUNT_MISMATCH' )
const itemVolumeWeight = draftCheck . items . reduce ( ( sum , value ) => sum + Number ( value . volumeWeightKg ) , 0 )
if ( Math . abs ( itemVolumeWeight - Number ( draftCheck . volumeWeightKg ) ) > 0.0000001 ) throw Error ( 'SERVER_ITEM_TOTAL_VOLUME_WEIGHT_ROUNDING_MISMATCH' )
entry . stage = 'weights' ; entry . weights = [ ] ; await save ( )
let netSoFar = 0
for ( let index = 0 ; index < shape . palletCount ; index ++ ) {
const pallet = pallets [ Number . parseInt ( hash ( ` ${ marker } : ${ index } ` ) . slice ( 0 , 8 ) , 16 ) % pallets . length ]
const upload = requireSuccess ( await ctx . requestMultipart ( 'POST' , ` /api/platform/v1/files/measurements/ ${ tenantId } / ${ inboundId } ` ,
{ content : PNG , type : 'image/png' , name : ` TEST-weight- ${ index + 1 } .png ` } ) )
const created = await api ( 'POST' , ` ${ base } /weight-results? ${ q } ` , { fileId : upload . fileId , palletId : pallet . id ,
ocrProvider : 'ADMIN_ACCEPTANCE' , ocrRawText : null , ocrWeightValue : null , ocrWeightUnit : null , confidence : null } )
const tare = Number ( created . palletTareWeightKg )
if ( created . palletTareWeightKg == null || ! Number . isFinite ( tare ) || tare < 0 ) throw Error ( 'PALLET_TARE_SNAPSHOT_MISSING' )
// Source gross is packed cargo weight, NOT scale weight including the reusable pallet.
const boxes = Math . min ( 30 , order . packageCount - index * 30 )
const net = index === shape . palletCount - 1 ? round ( order . weightKg - netSoFar , 3 )
: round ( order . weightKg * boxes / order . packageCount , 3 )
const confirmed = await api ( 'POST' , ` ${ base } /weight-results/ ${ created . id } /confirm? ${ q } ` ,
{ version : Number ( created . version ) , adjustedWeightValue : round ( net + tare , 3 ) , adjustedWeightUnit : 'KG' ,
adjustmentReason : ` ${ marker } TEST模拟称重:源货重按箱分摊,再加托盘皮重 ` } )
if ( Math . abs ( Number ( confirmed . netWeightKg ) - net ) > 0.001 ) throw Error ( 'CONFIRMED_NET_WEIGHT_MISMATCH' )
netSoFar += net
entry . weights . push ( { id : created . id , palletCode : pallet . palletCode , netWeightKg : net , tareWeightKg : tare } ) ; await save ( )
}
entry . stage = 'measurement-submit' ; await save ( )
await api ( 'POST' , ` ${ base } /measurement/submit? ${ q } ` , { measurementId : draft . measurementId , version : Number ( draft . version ) } )
const pending = await api ( 'GET' , inboundPath )
entry . stage = 'measurement-approve' ; await save ( )
await api ( 'POST' , ` /api/platform/v1/inbounds/ ${ inboundId } /measurements/approve? ${ q } ` ,
{ measurementId : draft . measurementId , expectedVersion : Number ( pending . summary ? . version ) , reason : 'TEST源数据流程试跑' } , ctx . token ,
{ 'Idempotency-Key' : ` ${ marker } -measurement-approve ` } )
const final = await poll ( ( ) => api ( 'GET' , measurementPath ) , ( data ) => data . processStatus === 'WAIT_SPLIT' )
entry . verification = { processStatus : final . processStatus , measurementStatus : final . measurementStatus ,
reviewStatus : final . measureReviewStatus , expectedPackages : order . packageCount , actualPackages : Number ( final . actualPackageCount ) ,
expectedCargoWeightKg : order . weightKg , actualNetWeightKg : Number ( final . actualNetWeightKg ) ,
sourceVolumeCbm : order . volumeCbm , measuredVolumeCbm : Number ( final . actualVolumeCbm ) ,
volumeDifferenceCbm : round ( Number ( final . actualVolumeCbm ) - order . volumeCbm ) ,
sourceVolumeMatched : Math . abs ( Number ( final . actualVolumeCbm ) - order . volumeCbm ) <= 0.0000001 ,
simulatedDimensions : entry . dimensionPlan , confirmedWeightImages : entry . weights . length }
if ( final . measureReviewStatus !== 'APPROVED' || final . measurementStatus !== 'APPROVED'
|| Number ( final . actualPackageCount ) !== order . packageCount
|| Math . abs ( Number ( final . actualNetWeightKg ) - order . weightKg ) > 0.001
|| ! entry . verification . sourceVolumeMatched ) throw Error ( 'FINAL_SOURCE_FACTS_MISMATCH' )
entry . stage = 'WAIT_SPLIT'
}
if ( process . argv [ 1 ] && import . meta . url === pathToFileURL ( resolve ( process . argv [ 1 ] ) ) . href ) {
main ( ) . catch ( ( error ) => { console . error ( JSON . stringify ( { error : error . message , evidence : error . evidence } ) ) ; process . exitCode = 1 } )
}