Files
baiyue-firstleg/scripts/real-order-xlsx-test-env.test.mjs
T
2026-09-09 20:53:24 +08:00

220 lines
13 KiB
JavaScript
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
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]
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' },
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' },
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)
})