import assert from 'node:assert/strict' import test from 'node:test' import { normalizeRows, channelName, matchRoute, measurementShape, buildSourceDraft, buildPayload, requireSuccess, executeCandidates, totalPreservingShapes, saveMeasurementDraft } from './real-order-xlsx-test-env.mjs' const header = ['进仓编号', '客户', '渠道', '亚马逊仓', '实际件数', '实际毛重', '实际立方', '预报件数', '买单/退税', '品名', 'SKU', 'FBA货件编号', '跟踪编号'] const row = ['ABCD2606010001', 'Example', '合德海卡包税', 'ABC1', 31, 100, 2, 31, '买单', 'Goods', 'SKU1', 'SHIP1', 'REF1'] const order = () => normalizeRows([header, row]).orders[0] test('Excel submit saves original key/version before uncertain transport and never auto-refreezes failed entries',async t=>{ const originalExitCode=process.exitCode t.after(()=>{process.exitCode=originalExitCode}) const value=order(),route=matchRoute(value,catalog),state={identity:{batchId:'frozen-submit'},orders:{}} const saved=[] const api=async(method,path,body,_token,headers)=>{ if(path.endsWith('/pricing/estimate'))return {channelCode:route.channel.channelCode,pricingTaxMode:value.pricingTaxMode} if(method==='POST'&&path.endsWith('/first-leg'))return 'order-frozen' if(method==='GET')return {order:{id:'order-frozen',version:8,orderNo:'FL',customerRemark:state.orders[value.key].marker},cargoes:[{packageCount:value.packageCount,grossWeightKg:value.weightKg,volumeCbm:value.volumeCbm}]} if(path.endsWith('/submit')){ const checkpoint=saved.at(-1).orders[value.key] assert.equal(checkpoint.stage,'submit');assert.deepEqual(checkpoint.submitCommand,{body,headers}) assert.deepEqual(body,{expectedVersion:8});throw new Error('transport lost') } throw new Error('unexpected call') } const options={candidates:[{order:value,route}],limit:1,state,save:async()=>saved.push(structuredClone(state)),api, ctx:{},merchant:'merchant',profile:{tenantName:'Example'},sharedResources:{productImageFileId:'image'},stage:'pending-review'} await executeCandidates(options) assert.equal(process.exitCode,1) assert.equal(state.orders[value.key].status,'FAILED') assert.equal(state.orders[value.key].submitCommand.body.expectedVersion,8) await executeCandidates({...options,api:async()=>{assert.fail('failed checkpoint must not issue another request')}}) assert.equal(state.orders[value.key].submitCommand.body.expectedVersion,8) }) const catalog = { receiving: { id: '1', warehouseName: 'Origin' }, channels: [{ channelName: '合德海卡', channelCode: 'CHANNEL', pricingTaxModes: ['TAX_INCLUDED'] }], destinations: [{ id: '2', warehouseType: 'FBA', warehouseCode: 'ABC1', warehouseName: 'ABC1', countryCode: 'US' }] } test('source counts remain actual, including after forecast differs', () => { const modified = [...row]; modified[7] = 99 assert.equal(normalizeRows([header, modified]).orders[0].packageCount, 31) }) test('missing and zero actual data do not fall back to forecast', () => { for (const missing of [null, '', 0]) { const modified = [...row]; modified[4] = missing assert.ok(normalizeRows([header, modified]).orders[0].problems.includes('MISSING_实际件数')) } }) test('invalid source codes are excluded, not converted into orders', () => { const modified = [...row]; modified[0] = 'container subtotal' const actual = normalizeRows([header, modified]) assert.equal(actual.orders.length, 0); assert.equal(actual.excluded.length, 1) }) test('explicit declaration changes use the stated final mode', () => { for (const [source, target] of [['买单改退税', 'TAX_REFUND'], ['退税改买单', 'BUY_DECLARATION']]) { const modified = [...row]; modified[8] = source const result = normalizeRows([header, modified]).orders[0] assert.equal(result.taxMode, target); assert.deepEqual(result.problems, []) } }) test('split rows aggregate actual counts once and reconcile forecast', () => { const a = [...row], b = [...row]; a[4] = 15; b[4] = 16 const result = normalizeRows([header, a, b]).orders[0] assert.equal(result.packageCount, 31); assert.deepEqual(result.rows, [2, 3]); assert.deepEqual(result.problems, []) b[4] = 15 assert.ok(normalizeRows([header, a, b]).orders[0].problems.includes('SPLIT_COUNT_MISMATCH')) }) test('split weight and volume totals use the persisted order precision', () => { const a = [...row], b = [...row] a[4] = 90; b[4] = 300; a[7] = 390; b[7] = 390 a[5] = 1137.461538; b[5] = 3791.538 a[6] = 7.256538462; b[6] = 24.188 const result = normalizeRows([header, a, b]).orders[0] assert.equal(result.weightKg, 4929) assert.equal(result.volumeCbm, 31.4445) }) test('conflicting channels on same inbound code block import', () => { const b = [...row]; b[2] = '其他包税' assert.ok(normalizeRows([header, row, b]).orders[0].problems.includes('CONFLICT_渠道')) }) test('channel suffix normalization never substitutes another base channel', () => { assert.equal(channelName('合德海派包税\n(FedEx)'), '合德海派') assert.equal(channelName('合德海卡不包税'), '合德海卡') assert.throws(() => matchRoute({ ...order(), channelName: 'Unknown' }, catalog)) }) test('excluded-tax estimation uses source billing unit and never guesses missing units', () => { const modified = [...row]; modified[2] = '合德海卡不包税' const columns = [...header, '单位'] const result = normalizeRows([columns, [...modified, 'CBM']]).orders[0] assert.equal(result.chargeMode, 'CBM'); assert.deepEqual(result.problems, []) assert.ok(normalizeRows([columns, [...modified, '']]).orders[0].problems.includes('UNKNOWN_OR_CONFLICTING_CHARGE_MODE')) }) test('ambiguous destinations and unavailable tax modes fail closed', () => { assert.throws(() => matchRoute(order(), { ...catalog, destinations: [...catalog.destinations, ...catalog.destinations] })) assert.throws(() => matchRoute({ ...order(), pricingTaxMode: 'TAX_EXCLUDED' }, catalog)) }) test('private addresses and dropship are explicit gaps, not random replacements', () => { assert.throws(() => matchRoute({ ...order(), destinationType: 'PRIVATE' }, catalog), /PRIVATE_ADDRESS/) assert.throws(() => matchRoute({ ...order(), fulfillmentType: 'ONE_CLICK_DROPSHIP' }, catalog), /WORK_PALLET/) }) test('logical pallet count uses ceiling of boxes divided by 30', () => { for (const [boxes, expected] of [[1, 1], [30, 1], [31, 2], [300, 10], [1000, 34]]) { assert.equal(measurementShape({ ...order(), packageCount: boxes }).palletCount, expected) } }) test('inferred groups conserve volume after ADD and server per-carton rounding', () => { const rules = [[30, 20, 20], [40, 30, 30], [50, 40, 40], [60, 50, 50]].map((dims, index) => ({ boxType: String(index), innerLengthCm: dims[0], innerWidthCm: dims[1], innerHeightCm: dims[2], edgeAdditionCm: 2 })) for (const [packageCount, volumeCbm] of [[8, 0.403], [17, 1.545], [7, 0.626], [31, 2], [300, 23.457], [1, 0.1], [1000, 98.7654]]) { const plan = totalPreservingShapes({ ...order(), packageCount, volumeCbm }, rules) assert.ok(plan.length <= 2) assert.equal(plan.reduce((n, g) => n + g.count, 0), packageCount) const ticks = plan.reduce((n, g) => { const raw = Number(((g.length + 2 * g.edgeAdditionCm) ** 3 / 1e6).toFixed(6)) return n + Math.ceil(raw * 10000 - 1e-8) * g.count }, 0) assert.equal(ticks, Math.round(volumeCbm * 10000)) if (Number.isInteger(Math.round(volumeCbm * 1e6) / 1000)) { const detailWeight = plan.reduce((sum, group) => sum + Math.ceil(group.unitVolumeCbm * group.count * 167 * 1000 - 1e-8), 0) assert.equal(detailWeight, Math.ceil(volumeCbm * 167 * 1000 - 1e-8)) } } assert.throws(() => totalPreservingShapes({ ...order(), volumeCbm: 0.123456 }, rules), /PRECISION/) }) test('source-only draft does not invent measurement or product evidence', () => { const payload = buildSourceDraft(order(), matchRoute(order(), catalog), 'Example', 'MARKER') assert.equal(payload.cargoes[0].packageCount, 31) assert.equal(payload.cargoes[0].grossWeightKg, 100) assert.equal(payload.cargoes[0].volumeCbm, 2) assert.equal(payload.cargoes[0].woodenProduct, false) for (const key of ['lengthCm', 'productImageFileId', 'hsCode', 'dangerousGoods']) assert.equal(key in payload.cargoes[0], false) }) test('simulated payload is labeled, preserving source measures without randomization', () => { const payload = buildPayload(order(), matchRoute(order(), catalog), 'Example', 'image', 'MARKER') assert.match(payload.customerRemark, /非真实测量证据/) assert.equal(payload.cargoes[0].volumeCbm, 2) assert.match(payload.cargoes[0].cargoRemark, /禁止用于真实报关/) }) test('HTTP business errors preserve diagnostic evidence', () => { assert.throws(() => requireSuccess({ http: 200, path: '/example', body: { code: 'A0409' }, requestId: 'req' }), (error) => error.evidence.code === 'A0409' && error.evidence.requestId === 'req') }) test('measurement draft retries a transient server error after refreshing the version', async () => { let reads = 0, writes = 0 const retries = [] const entry = { dimensionPlan: [{ itemId: 'item-1', count: 31, length: 40, width: 30, height: 30 }] } const api = async (method) => { if (method === 'GET') return { measurementId: 'measurement-1', version: ++reads, items: [{ id: 'item-1', inboundDetailId: 'detail-1', itemNo: 1, boxSpecification: 'TEST' }] } writes++ if (writes === 1) { const error = Error('temporary'); error.evidence = { code: 'S0500', requestId: 'req-1' }; throw error } return { measurementId: 'measurement-1', version: 3 } } const result = await saveMeasurementDraft({ api, measurementPath: '/measurement', base: '/inbound/1', q: 'tenantId=1', order: order(), entry, marker: 'MARKER', wait: async () => {}, onRetry: async (retry) => retries.push(retry) }) assert.equal(result.version, 3) assert.equal(reads, 2) assert.equal(writes, 2) assert.deepEqual(retries.map((retry) => retry.code), ['S0500']) }) test('same batch never recreates an attempted order, even when interrupted', async () => { const value = order(), state = { identity: { batchId: 'test' }, orders: { [value.key]: { status: 'STARTED' } } } await executeCandidates({ candidates: [{ order: value, route: catalog }], limit: 1, state, save: async () => {}, api: async () => { throw Error('must not call HTTP') } }) assert.equal(state.lastRun.attempted, 0); assert.equal(state.lastRun.skipped, 1) }) test('pending-review stage submits but never calls approval or measurement APIs', async () => { const value = order(), route = matchRoute(value, catalog) const state = { identity: { batchId: 'pending-review' }, orders: {} } const calls = [] let detailReads = 0 const api = async (method, path) => { calls.push(`${method} ${path}`) if (path.endsWith('/pricing/estimate')) return { channelCode: route.channel.channelCode, pricingTaxMode: value.pricingTaxMode } if (method === 'POST' && path === '/api/merchant/v1/orders/first-leg') return 'order-1' if (path === '/api/merchant/v1/orders/first-leg/order-1' && method === 'GET') { detailReads++ return { order: { id: 'order-1', orderNo: 'FL-1', customerRemark: Object.values(state.orders)[0].marker, status: detailReads > 1 ? 'SUBMITTED' : 'DRAFT', version: 7 }, cargoes: [{ packageCount: value.packageCount, grossWeightKg: value.weightKg, volumeCbm: value.volumeCbm }] } } if (path === '/api/merchant/v1/orders/order-1') return { summary: { mainStatus: 'PENDING_REVIEW' } } if (method === 'POST' && path.endsWith('/submit')) return true throw Error(`unexpected call ${method} ${path}`) } await executeCandidates({ candidates: [{ order: value, route }], limit: 1, concurrency: 1, state, save: async () => {}, api, merchant: 'merchant', profile: { tenantName: 'Example' }, sharedResources: { productImageFileId: 'image-1' }, stage: 'pending-review' }) assert.equal(Object.values(state.orders)[0].stage, 'PENDING_REVIEW') assert.equal(Object.values(state.orders)[0].verification.mainStatus, 'PENDING_REVIEW') assert.equal(calls.some((call) => /approve|measurement|inbounds/.test(call)), false) }) test('pending-review retries an estimate-only pricing failure and records it as a warning', async () => { const value = order(), route = matchRoute(value, catalog) const state = { identity: { batchId: 'pricing-warning' }, orders: { [value.key]: { status: 'FAILED', stage: 'estimate', error: 'HTTP_200_CODE_A0404', evidence: { code: 'A0404', requestId: 'old-request' }, finishedAt: '2026-01-01T00:00:00Z' } } } let detailReads = 0 const api = async (method, path) => { if (path.endsWith('/pricing/estimate')) { const error = Error('no price'); error.evidence = { code: 'A0404', message: 'no price' }; throw error } if (method === 'POST' && path === '/api/merchant/v1/orders/first-leg') return 'order-2' if (path === '/api/merchant/v1/orders/first-leg/order-2' && method === 'GET') { detailReads++ return { order: { id: 'order-2', orderNo: 'FL-2', customerRemark: Object.values(state.orders)[0].marker, status: detailReads > 1 ? 'SUBMITTED' : 'DRAFT', version: 7 }, cargoes: [{ packageCount: value.packageCount, grossWeightKg: value.weightKg, volumeCbm: value.volumeCbm }] } } if (path === '/api/merchant/v1/orders/order-2') return { summary: { mainStatus: 'PENDING_REVIEW' } } if (method === 'POST' && path.endsWith('/submit')) return true throw Error(`unexpected call ${method} ${path}`) } await executeCandidates({ candidates: [{ order: value, route }], limit: 1, concurrency: 1, state, save: async () => {}, api, merchant: 'merchant', profile: { tenantName: 'Example' }, sharedResources: { productImageFileId: 'image-1' }, stage: 'pending-review' }) const entry = state.orders[value.key] assert.equal(entry.status, 'SUCCEEDED') assert.equal(entry.pricingWarning.code, 'A0404') assert.equal(entry.priorAttempts.length, 1) }) test('single process respects bounded concurrency while keeping per-order checkpoints', async () => { const base = order() const route = matchRoute(base, catalog) const candidates = Array.from({ length: 7 }, (_, index) => ({ order: { ...base, key: `key-${index}`, sourceCode: `SRC-${index}`, rows: [index + 2] }, route, })) const state = { identity: { batchId: 'concurrency' }, orders: {} } let active = 0, maximum = 0, id = 0 const api = async (method, path, body) => { if (path.endsWith('/pricing/estimate')) { active++; maximum = Math.max(maximum, active) await new Promise((resolve) => setTimeout(resolve, 5)) active-- return { channelCode: route.channel.channelCode, pricingTaxMode: base.pricingTaxMode } } if (method === 'POST') return String(++id) const marker = path.split('/').at(-1) return { order: { id: marker, orderNo: `ORDER-${marker}`, customerRemark: Object.values(state.orders) .find((entry) => entry.orderId === marker)?.marker }, cargoes: [{ packageCount: 31, grossWeightKg: 100, volumeCbm: 2 }] } } await executeCandidates({ candidates, limit: 7, concurrency: 3, state, save: async () => {}, api, profile: { tenantName: 'Example' }, stage: 'create' }) assert.equal(maximum, 3) assert.equal(state.lastRun.attempted, 7) assert.equal(state.lastRun.succeeded, 7) assert.equal(Object.values(state.orders).filter((entry) => entry.status === 'SUCCEEDED').length, 7) })